bufio通过缓存来提高效率,缓存放在主存中。
Golang的bufio
包实现了带缓存的I/O读写操作,用来帮助处理I/O
缓存。
通过缓存可以提高效率,把文件读取进缓存(内存)后再读取的时候可避免文件系统的I/O,从而提高速度。同理,当进行写操作时会先把文件写入缓存(内存),然后由缓存写入文件系统。缓冲区的设计目的是为了存储多次的写入,最后一次性将缓冲区内容写入文件。
为什么bufio
性能会比io
性能要高呢?
以缓存读为例,当设置好缓冲区大小以及读取字节数和缓冲字节数后。
- 若缓冲区为空,同时读取字节数大于等于缓冲字节数,则直接从文件中读取,即不启用缓冲。
- 若缓冲区为空,同时读取字节数小于缓冲区字节数,则从文件中读取缓冲区字节内容到缓冲区后,程序再从缓冲区中读取内容,此时缓冲区大小会等于缓冲区大小减去缓冲区字节数。
- 若缓冲区不为空,同时读取字节数小于缓冲字节数,则会从缓冲区读取内容,此时不会发生文件I/O。
- 若缓冲区不为空,同时读取字节数大于等于缓冲字节数,则会置空缓冲区,重新读取。
只有当缓冲区中存在内容,同时程序此次读取是从缓冲区读取时不会发生文件I/O。只有当缓冲区为空时,才会发生文件I/O。如果缓冲区大小足够,则会启用缓冲读,先将内容载入填满缓冲区,程序再从缓冲区中读取。如果缓冲区过小,则会直接从文件中读取,而不会使用缓冲读。
以缓冲写为例,当设置为缓冲区大小、写入字节数、缓冲字节数。
缓冲区 | 缓冲写 | 文件I/O | 描述 |
---|---|---|---|
空 | 写入字节数 >= 缓冲字节数 | 有 | 直接写入文件,不启用缓冲 |
空 | 写入字节数 < 缓冲字节数 | 无 | 写入缓冲区 |
非空 | 写入字节数 + 缓冲字节数 < 缓冲区大小 | 无 | 写入缓冲区 |
非空 | 写入字节数 + 缓冲字节数 >= 缓冲区大小 | 有 | 将缓冲区字节写入文件 |
bufio
bufio
包封装了io.Reader
和io.Writer
对象,具有缓存和文本读写功能。
bufio
包提供了两个New
函数NewReader()
和NewWriter()
,分别在任意io.Reader
和io.Writer
的基础上再包装一层缓存区得到bufio.Reader
和bufio.Writer
。
func bufio.NewReader(rd io.Reader) *bufio.Reader
func bufio.NewWriter(w io.Writer) *bufio.Writer
对象 | 描述 |
---|---|
bufio.Reader | 带缓存区的字节流读取器 |
bufio.Writer | 带缓存区的字节流写入器 |
可用于文件I/O操作,先将数据缓存到内存中,再整体做文件I/O操作,尽最大可能地减少磁盘I/O。
bufio.Reader
通过bufio.Reader
可从底层的io.Reader
中更大批量地读取数据,使读取操作减少。
若数据读取时的块数量是固定合适的,底层媒体设备将会有更好的表现,也因此会提高程序的性能。
io.Reader --> buffer --> consumer
假如:消费者想要从磁盘上读取10个字符,每次读取一个字符。在底层实现上会触发10次读取操作。若磁盘按每个数据块4个字节来读取数据,则bufio.Reader
会起到帮助作用。底层引擎会存储整个数据块,然后提供一个可以挨个读取字节的API给消费者。
// 缓存读取器
type Reader struct {
buf []byte // 缓存
rd io.Reader // 底层的io.Reader
r, w int
err error // 读过程中遇到的错误
lastByte int // 最后一次读到的字节
lastRuneSize int // 最后一次读到的Rune的大小
}
字段 | 类型 | 描述 |
---|---|---|
buf | []byte | 缓存 |
fd | io.Reader | 消费者,读取器。 |
err | error | 缓存读取时错误 |
lastByte | int | 最后一次读取到的字节 |
lastRuneSize | int | 最后一次读取到的Rune大小 |
bufio.NewReaderSize
func bufio.NewReaderSize(rd io.Reader, size int) *bufio.Reader
-
NewReaderSize
会将rd
封装成一个带缓存的buf.Reader
对象 - 缓存大小由
size
指定,若小于16字节则会被设置为16。 - 如果
rd
的基类型是拥有足够缓存的bufio.Reader
类型则直接将rd
转换为基类型后返回。
bufio.NewReader
-
bufio.NewReader()
相当于bufio.NewReaderSize(rd, 4096)
func bufio.NewReader(rd io.Reader) *bufio.Reader
reader.Read
-
bufio.Read(p []byte)
相当于读取大小为len(p)
的内容
func (b *Reader) Read(p []byte) (n int, err error)
读取思路
- 当
buf
缓存区有内容时,将缓存区内容全部填入p
并清空缓存区。 - 当
buf
缓存区没内容时
2.1.len(p) > len(buf)
,即待读取内容比缓存区要大,直接去文件读取即可。
2.2.len(p) < len(buf)
,即待读取内容比缓存区小,缓存区从文件读取内容充满缓存区,并将p
填满,此时缓存区有剩余内容。 - 再次读取时缓存有内容,则将缓存区内容全部填入
p
并清空缓存区。
- 缓冲读通过预读,可以在一定程序上减少文件I/O次数以提高性能。
//原始字符串
str := "12345678901234567890123456789012345678901234567890"
//将字符串转换为流式I/O对象
strReader := strings.NewReader(str)
//设置读缓存区大小
bufReader := bufio.NewReaderSize(strReader, 16)//16byte
//缓存读
p := make([]byte, 16)
n,err := bufReader.Read(p)
if err!=nil{
panic(err)
}
buffered := bufReader.Buffered()
fmt.Printf("buffered:%d, content:%s\n", buffered, p[:n])
buffered:0, content:1234567890123456
当缓存区中有内容时,程序的读取会从缓存区读而不会发生文件I/O。只有当缓存区为空时才会发生文件I/O
若缓存区的大小足够则启用缓存读,先会将内容载入填满缓存区,程序再从缓存区中读取。若缓存区过小则会直接从文件中读取而不使用缓存读
reader.Buffered
-
reader.Buffered()
方法返回读取器从当前缓冲区读取的字节数
func (b *Reader) Buffered() int { return b.w - b.r }
bufio.Writer
多次少量写操作会影响程序性能,因为每次写操作最终都会体现为系统层调用,频繁写操作可能对CPU造成伤害。而且很多硬件设备更适合处理块对齐的数据,比如硬盘。
为减少多次写操作所需的开支,Golang提供了bufio.Writer
。
type Writer struct {
err error
buf []byte//缓存存储数据
n int//缓存内部当前操作的位置
wr io.Writer//消费者写入器
}
字段 | 类型 | 描述 |
---|---|---|
err | error | I/O错误 |
buf | []byte | 缓存,存储数据。 |
n | int | 缓存内部当前操作的位置 |
wr | io.Writer | 消费者,写入器。 |
使用bufio.Writer
将不再直接写入目的地(实现了io.Writer
接口),而是先写入缓存,当缓存写满后再统一写入目的地。
producer --> buffer --> io.Writer
bufio.Writer
底层使用[]byte
进行缓存,字段buf
用于存储数据,当缓存满或Flush
被调用时,消费者wr
可以从缓存中读取到数据。若写入过程中发生了I/O error
,此error
将会被赋给err
字段,error
发生之后,writer
将会停止操作(writer is no-op
)。
bufio.Writer
默认使用4096长度字节的缓存,可使用NewWriterSize()
方法来设置。