缓存存在的作用
Preface
不管是在什么编程语言中,在执行I/O
相关的操作时,通常会为其添加一层缓存,如在golang
中执行写文件操作这样:
func main() {
filename := "test.txt"
content := "Hello World!"
fileObj, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
if err != nil {
return
}
defer fileObj.Close()
bufWr := bufio.NewWriter(fileObj)
if _, err := bufWr.Write([]byte(content)); err != nil {
return
}
bufWr.Flush()
}
我们将文件句柄传递给一个缓存,创建一个bufio.Writer
,然后通过这个bufio.Writer
来完成写操作;有时候我们其实并不知道为什么要这样做,也不知道这样做究竟能带来什么好处,下面就来具体的分析一下
Why
首先我们看一下在golang
中I/O
的结构:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
I/O
最基本的只需要两个方法:Read,Write
;Read
是将文件中的内容读取出来放在p
字节数组当中,然后返回给消费者使用,也就是执行一次读盘操作(此处也不一定必须是磁盘);Write
是将p
字节数组当中的内容写入磁盘当中,即执行一次写盘操作
我们知道磁盘和内存的读写速度相差很大,所以在写程序时对于性能要求比较高的代码就需要减少读盘写盘的操作;而如果现在有一个程序要求频繁的读写盘,那应该如何提高程序性能呢?答案就是使用缓存,下面就说说缓存到底是如何提升读写盘效率的
How
缓存的本质就是在内存开辟一块空间,当执行读盘操作的时候,先将数据读到这块内存当中然后再从这块内存中返回指定大小的字节数给消费者(此处的重点就是返回指定大小的字节数),就如下图所示:
上面的图中,先是将数据从文件读取到buffer
当中,然后再返回给消费者所需要的字节数;下面就是一个例子:
func main() {
filename := "test.txt"
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
bufReader := bufio.NewReader(f)
buf := make([]byte, 64)
for i := 0; i < 10; i++ {
_, err = bufReader.Read(buf)
if err != nil {
return
}
fmt.Printf("Content:%s\n", string(buf))
}
}
上面的代码就是从文件中读取十次数据,每次只读取64
个字节,我们通过bufio.NewReader
创建一个缓存,这个缓存默认大小是4096
个字节,这样我们只需要执行一次读盘操作就可以了
When
看完上的例子,感觉这个缓存有点多此一举的意思;如果一次直接读取4096
个字节,那还需要什么缓存呢?
事实确实如此,当你读取的数据超过缓存的大小,那确实就没缓存啥事了,而bufio.Reader
中的Read
也确实是这么实现的,只要读取的数据p
字节数组大于或者等于缓存的大小,就会直接使用文件的句柄读写,而不再往缓存写任何数据了
下面是bufio.Reader
的源码结构:
// Reader implements buffering for an io.Reader object.
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
读取的数据就会缓存再buf
数组当中,这个数组默认大小为4096
,当然也可以通过bufio.NewReaderSize
来指定这个缓存的大小
下面是bufio.Reader
的Read
方法:
func (b *Reader) Read(p []byte) (n int, err error) {
n = len(p)
if n == 0 {
if b.Buffered() > 0 {
return 0, nil
}
return 0, b.readErr()
}
if b.r == b.w {
if b.err != nil {
return 0, b.readErr()
}
if len(p) >= len(b.buf) {
// Large read, empty buffer.
// Read directly into p to avoid copy.
n, b.err = b.rd.Read(p)
if n < 0 {
panic(errNegativeRead)
}
if n > 0 {
b.lastByte = int(p[n-1])
b.lastRuneSize = -1
}
return n, b.readErr()
}
// One read.
// Do not use b.fill, which will loop.
b.r = 0
b.w = 0
n, b.err = b.rd.Read(b.buf)
if n < 0 {
panic(errNegativeRead)
}
if n == 0 {
return 0, b.readErr()
}
b.w += n
}
// copy as much as we can
// Note: if the slice panics here, it is probably because
// the underlying reader returned a bad count. See issue 49795.
n = copy(p, b.buf[b.r:b.w])
b.r += n
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = -1
return n, nil
}
上面的代码中可以看到当len(p)>=len(b.buf)
的时候就直接用文件句柄读取了,这样就能够避免多一次的copy
了
Similarly
对于bufio.Writer
也是一样的道理,io.Writer
只有一个方法Write(p []byte) (n int,err error)
;其作用就是将p
字节数组中的数据写入磁盘(此处并不一定是磁盘),而如果每次只写入一点,也就是指这个p
字节数组很小;那这样就会执行多次写盘操作
而使用bufio.Writer
就可以先将数据写入缓存当中,等缓存满了或者是数据全部写完之后就可以将数据写入磁盘当中了,这样就能够减少写盘的操作了,如下图所示:
同样的,如果一次写入的数据len(p)>=len(b.buf)
那么就不会缓存这些数据了,而是直接写入磁盘当中了;当然,也可以使用bufio.NewWriterSize
来指定缓存的大小
End
不管是读缓存还是写缓存,都是以空间换时间来提升程序的性能;但是,如果开辟过大的内存空间来缓存这些数据也会带来性能上的影响
最后一点就是在执行缓存读写的时候一定要注意执行Flush
操作,将数据同步回去,防止内存的泄露
转载自:https://juejin.cn/post/7218379300504600613