vps网站空间,wordpress的模板,优化方案英语必修三电子版,深圳网站seo关键词缓冲 IO 计算机中我们常听到这样的两种程序优化方式#xff1a;
以时间换空间以空间换时间
今天要来看的缓冲IO就是典型的以空间换时间#xff0c;它的基本原理见上图。简单的解释就是#xff1a;程序不再直接去读取底层的数据源#xff0c;而是通过一个缓冲区来进行读取…缓冲 IO 计算机中我们常听到这样的两种程序优化方式
以时间换空间以空间换时间
今天要来看的缓冲IO就是典型的以空间换时间它的基本原理见上图。简单的解释就是程序不再直接去读取底层的数据源而是通过一个缓冲区来进行读取。即先把数据从底层数据源读取到缓冲区然后再从缓冲区进行读取。这样一分析似乎有点多此一举了本来一次的数据读写变成了两次其实不是这样的因为这里的两个读写的成本或者说代价不同。我们都知道存储器是分级的硬盘读写速度和内存读写速度之间的差距是巨大的内存读写 硬盘读写或者网络读写。
所以缓冲区的真正作用是减少了硬盘或者网络读写的次数读写的数据量没变。
举一个例子我有1024 个字节的数据每次读取 32 个byte那么需要访问硬盘或者网络 32次。如果一次读取 1024 个字节呢最终只需要一次硬盘或者网络访问就够了内存访问的次数增加但是总的耗时是减少的。不过我们再扩展一点既然内存读写这么快我们直接把文件一次性读入内存不就好了这样的想法是好的但是对于大文件GB级或者更大的我想你是不会全部读取进内存的因为内存资源也是有限的。
好了前面是一些基本的概念的复习简单回顾一下有助于我们理解接下来的内容。今天来记录一下我阅读 golang 的 bufio 包的理解 Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O. 这是 bufio 包自己的介绍从它的名字来理解就是缓冲 IO。
缓冲读
因为这个 bufio.go 文件还是蛮大的所以准备分成两个缓冲读和写部分进行阅读。接下来会介绍几个我认为比较重要的方法会有一些自己的理解。
Buffered input 缓冲输入
const (defaultBufSize 4096
)// Buffered input.// Reader implements buffering for an io.Reader object.
type Reader struct {buf []byterd io.Reader // reader provided by the clientr, w int // buf read and write positionserr errorlastByte int // last byte read for UnreadByte; -1 means invalidlastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}const minReadBufferSize 16
const maxConsecutiveEmptyReads 100这就是缓冲输入 Reader 的结构体它有一个 buf 字节切片默认的长度是4096即 4KB并且依赖 io.Reader 实现的。这里可以看出来所谓的缓冲区其实就是一个字节切片。 所以如果缓冲区是空的判断条件即是r.b r.w。
fill 填充数据
fill() 是内部用来填充缓冲区的方法可以说能实现缓冲就是依赖它来实现的。
// fill reads a new chunk into the buffer.
func (b *Reader) fill() {// Slide existing data to beginning.if b.r 0 {copy(b.buf, b.buf[b.r:b.w])b.w - b.rb.r 0}if b.w len(b.buf) {panic(bufio: tried to fill full buffer)}// Read new data: try a limited number of times.for i : maxConsecutiveEmptyReads; i 0; i-- {n, err : b.rd.Read(b.buf[b.w:])if n 0 {panic(errNegativeRead)}b.w nif err ! nil {b.err errreturn}if n 0 {return}}b.err io.ErrNoProgress
}这里是我从源码中的理解 如果 b.r 0这说明缓冲区已经被读取了一些数据所以要把后面未读取的数据搬到前面来然后 b.w 就要减少 b.r同时 b.r 需要置零因为已经搬到前面了。 如果 b.w len(b.buf)这种属于异常情况会发生 panic(bufio: tried to fill full buffer)。也就是说当你尝试向一个满的 buf 切片填充数据时这是不允许的。这是出于程序正确性的角度考虑的。 然后就可以尝试进行填充数据了。这里很有意思填充数据本身是在一个循环中进行的它最多会执行 100 次[1, 100]。
循环体内的内容 从底层的 reader 中读取数据把读取的数据存入缓冲区的切片中b.w 开始。 如果读取的 n 0那么会发生 panic。这种都是专门针对各种异常情况的处理通常是不会发生的。 把数据写入缓冲区后b.w 同时向后移动到数据的最后面。 如果读取数据时发生了错误那么就返回。
如果 n 0那么就返回。这里很奇怪为什么要单独 n 0 时返回呢因为还有一种可能是 n 0如果出现这种情况说明没有读取到数据。那么怎么办呢这就是前面循环的作用了它会再次尝试读取如果尝试 100 次都失败了那么就停止读取并且返回错误。这种情况通常是因为一个错误的 reader 实现导致的 这里举一个例子测试字符串 CrazyDragon假设初始缓冲区容量为10现在开始第一次填充数据r.b r.w 0假设它读取了Crazy 这 5 个数据这里的读取是从底层数据源读取对于缓冲区来说它是指写入。因为还没有开始读取这里的读取指的是对缓冲区的读取所以 r.b 为 0r.w 为 5这里你可以思考一下为什么不是 4 呢。 然后的操作的是
用户读取了 3 个数据那么现在 r.b 移动到 3 的位置然后再次触发填充函数。它会首先把从 r.b 到 r.w 的数据搬到或者说复制缓冲切片的开头.然后从底层数据源读取 Dragon 这 6 个数据并将其填充进缓冲区r.w 向后移动 6。 Peak 查看但不读取数据
注// Tip: 以这个开头的中文注释是我添加的理解。
// Peek returns the next n bytes without advancing the reader. The bytes stop
// being valid at the next read call. If Peek returns fewer than n bytes, it
// also returns an error explaining why the read is short. The error is
// ErrBufferFull if n is larger than bs buffer size.
//
// Calling Peek prevents a UnreadByte or UnreadRune call from succeeding
// until the next read operation.
func (b *Reader) Peek(n int) ([]byte, error) {if n 0 {return nil, ErrNegativeCount}b.lastByte -1b.lastRuneSize -1// Tip:如果 n 大于缓冲区剩余的数据且缓冲区未满会调用 fill() 将其填满for b.w-b.r n b.w-b.r len(b.buf) b.err nil {b.fill() // b.w-b.r len(b.buf) buffer is not full}// Tip:如果 n 大于缓冲区的长度会报错 ErrBufferFullif n len(b.buf) {return b.buf[b.r:b.w], ErrBufferFull}// 0 n len(b.buf)var err error// Tip:如果最终还是剩余数据小于需要读取的数据那么就返回剩余数据和相应的错误if avail : b.w - b.r; avail n {// not enough data in buffern availerr b.readErr()if err nil {err ErrBufferFull}}// Tip:这才是最关键的代码只是返回缓冲区中数据的切片其他的都错误处理相关的return b.buf[b.r : b.rn], err
}Peek 有偷看的意思它的作用就是查看当前读取位置之后的 n 字节数据但是不移动读取的下标。如果它返回的字节少于 n 个它会返回一个错误来解释原因。如果 n 大于缓冲区的大小那么会返回一个 ErrBufferFull 错误。通过调用 Peek() 可以阻止 UnreadByte 或者 UnreadRune 调用成功直到下一次读取操作。
Discard 丢弃数据
// Discard skips the next n bytes, returning the number of bytes discarded.
//
// If Discard skips fewer than n bytes, it also returns an error.
// If 0 n b.Buffered(), Discard is guaranteed to succeed without
// reading from the underlying io.Reader.
func (b *Reader) Discard(n int) (discarded int, err error) {if n 0 {return 0, ErrNegativeCount}if n 0 {return}b.lastByte -1b.lastRuneSize -1// Tip:因为缓冲区里面不一定有足够的数据所以它也是会在底层调用 fill 来填充数据的remain : nfor {skip : b.Buffered()if skip 0 {b.fill()skip b.Buffered()}if skip remain {skip remain}b.r skipremain - skipif remain 0 {return n, nil}if b.err ! nil {return n - remain, b.readErr()}}
}discard 就是丢弃的意思这个词在 IT 领域还是很常见的特别是和网络相关的。它的作用就是丢弃指定的 n 个字节返回实际丢弃的字节数和相应的错误。
Read 读取数据
这个方法使用起来和正常的 IO 的 Read 是一样的主要区别在于是先从缓冲区取数据可能会调用一次 fill 函数填充缓存区。ReadByte 它返回下一个字节ReadRune它返回下一个 Unicode 字符。
UnReadByte 和 UnrReadRune 往前读
这里我把它称为往前读因为它的作用是把读取指针向前移动。这两个方法类似不过一个是向前移动一个字节另一个是向前移动一个字符。
这里可以看到向前移动一个字节后读取指针又指向了 :本来这个字符已经被读取出来了指针已经执行它后面的那个位置了。
package mainimport (bufiofmtstrings
)func main() {reader : strings.NewReader(CrazyDragon: I love you yesterday and today.)bufReader : bufio.NewReaderSize(reader, 16)// 长度设置为 12正好读取到 CrazyDragon:line : make([]byte, 12)n, err : bufReader.Read(line)if err ! nil {fmt.Println(err)}fmt.Println(string(line[:n]))// 缓冲区大小是 16读取 12还剩下 4fmt.Println(剩余缓冲数据, bufReader.Buffered())// 调用 UnreadByte读取指针前移一位此时缓冲区还剩下 5bufReader.UnreadByte()fmt.Println(bufReader.Buffered())n, err bufReader.Read(line[:1])if err ! nil {fmt.Println(err)}fmt.Println(string(line[:n]))ReadSlice 读取一个切片
// ReadSlice reads until the first occurrence of delim in the input,
// returning a slice pointing at the bytes in the buffer.
// The bytes stop being valid at the next read.
// If ReadSlice encounters an error before finding a delimiter,
// it returns all the data in the buffer and the error itself (often io.EOF).
// ReadSlice fails with error ErrBufferFull if the buffer fills without a delim.
// Because the data returned from ReadSlice will be overwritten
// by the next I/O operation, most clients should use
// ReadBytes or ReadString instead.
// ReadSlice returns err ! nil if and only if line does not end in delim.
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {s : 0 // search start indexfor {// Search buffer.if i : bytes.IndexByte(b.buf[b.rs:b.w], delim); i 0 {i sline b.buf[b.r : b.ri1]b.r i 1break}// Pending error?if b.err ! nil {line b.buf[b.r:b.w]b.r b.werr b.readErr()break}// Buffer full?if b.Buffered() len(b.buf) {b.r b.wline b.buferr ErrBufferFullbreak}s b.w - b.r // do not rescan area we scanned beforeb.fill() // buffer is not full}// Handle last byte, if any.if i : len(line) - 1; i 0 {b.lastByte int(line[i])b.lastRuneSize -1}return
}这个方法的作用是从缓冲区中读取数据在第一次遇到指定的分隔符后停止并返回一个指向缓冲区中这些字节的切片。ReadSlice 如果缓冲区填满数据但是没有一个分隔符它会返回一个 ErrBufferFull 错误。
因为从 ReadSlice 返回的数据会被下一次的 IO 操作覆盖所以大多数客户端应该使用 ReadBytes 或者 ReadString 来替代。
ReadSlice 只有在 line 没有结束在分隔符时才会返回 err ! nil。
这里来做一个实验测试数据为CrazyDragon: I love you yesterday and today因为默认的缓冲区大小是 4096 字节这个对于测试代码太大了。但是它也是可以设置的最小值是 16 字节这里我就设置成最小值来测试。
package mainimport (bufiofmtstrings
)func main() {reader : strings.NewReader(CrazyDragon: I love you yesterday and today.)bufReader : bufio.NewReaderSize(reader, 16)// 此时还没有开始填充如果缓冲区是空的输出是 0fmt.Println(Buffered: , bufReader.Buffered())// 开始从缓冲区读取底层会调用 fill() 函数进行填充的line, err : bufReader.ReadSlice(:)if err ! nil {fmt.Println(err)}// 这时 r.b 已经移动到相应的位置了此时缓冲区的长度为 4fmt.Println(Buffered: , bufReader.Buffered())fmt.Println(string(line))// 再次尝试读取因为找不到分隔符所以报错了_, err bufReader.ReadSlice(:)if err ! nil {fmt.Println(Error: , err)}// 查看此时整个缓冲区的数据line, err bufReader.Peek(16)if err ! nil {fmt.Println(Error: , err)}fmt.Printf(len(line) %d, line %s, len(line), string(line))
}所以可见 Buffered() 输出结果是 4r.w 为 16r.b 为 12。不过这种情况就是从头开始读取一个满的缓冲区如果缓冲区本身是未满的那么它会将缓冲区填满不一定是一次填满可能会多次执行 fill()这里的情况还是很复杂的对于网络流来说每次你都不一定可以读取到数据但是如果缓存区满了还是没有发现分隔符那么就会报错。 ReadLine 读取一行
ReadLine 是一个低级的读取行方法。大多数调用者应该使用 ReadBytes(\n) 或者 ReadString(\n) 替代或者使用扫描器 Scanner。实际上它在内部是这样调用的line, err b.ReadSlice(\n)。 不过也要注意它的不同点它是不会返回行结束符字节的。也就是说: ReadSlice(\n) 返回的数据是包含 \n但是 ReadLine() 是不会包含的。
如果遇到了 ErrBufferFull 错误说明整个缓存区都没有遇到 \n。但是它还会考虑是否存在 \r\n 横跨缓冲区的情况即 \r 是缓冲区的最后一个字节然后 \n 还没有到来不过这只能等待下一次调用取检查了可以通过第二个返回值来判断是否是这种情况。
这里来测试一下
func ReadLineTest(s string) {reader : strings.NewReader(s)bufReader : bufio.NewReaderSize(reader, 16)line, isPrefix, err : bufReader.ReadLine()if err ! nil {fmt.Println(err)}fmt.Printf(line %s, isPrefix %v\n, string(line), isPrefix)
}ReadLineTest(CrazyDragonKing\r\n)
ReadLineTest(CrazyDragonKing\r)
ReadLineTest(CrazyDragonKing\n)
ReadLineTest(CrazyDragonKingCrazyDragonKingCrazyDragonKing)这里来总结一下各种可能的情况注缓冲区大小为 16CrazyDragonKing 长度是 15
CrazyDragonKing\r\n CrazyDragonKing, true, nil
CrazyDragonKing\r CrazyDragonKing, true, nil
CrazyDragonKing\n CrazyDragonKing, false, nil
CrazyDragonKingCrazyDragonKingCrazyDragonKing CrazyDragonKingC, true, nilReadBytes 和 ReadString 读取字节切片和读取字符串
ReadSlice 是当遇到分隔符之后返回字节切片包含分隔符本身它会尝试填满缓冲区如果没有遇到分隔符会返回 ErrBufferFull 错误。
ReadLine 内部依赖 ReadSlice 实现它返回的也是字节切片不包含分隔符本身。
ReadBytes 和 ReadString 它们两个一个返回字节切片一个返回字符串。不同的地方在于如果一个缓冲区没有找到它们不会返回 ErrBufferFull 错误。而是继续读取底层的数据源直到遇到分隔符或者文件结束。
这里来做一个实验
func ReadStringTest(s string) {reader : strings.NewReader(s)bufReader : bufio.NewReaderSize(reader, 16)line, err : bufReader.ReadString(\n)if err ! nil {fmt.Println(err)}fmt.Printf(line %s\n, line)
}ReadLineTest(CrazyDragonKingCrazyDragonKingCrazyDragonKing)
ReadStringTest(CrazyDragonKingCrazyDragonKingCrazyDragonKing\nCrazyDragonKing\n)collectFragments 收集片段
// collectFragments reads until the first occurrence of delim in the input. It
// returns (slice of full buffers, remaining bytes before delim, total number
// of bytes in the combined first two elements, error).
// The complete result is equal to
// bytes.Join(append(fullBuffers, finalFragment), nil), which has a
// length of totalLen. The result is structured in this way to allow callers
// to minimize allocations and copies.
func (b *Reader) collectFragments(delim byte) (fullBuffers [][]byte, finalFragment []byte, totalLen int, err error) {var frag []byte// Use ReadSlice to look for delim, accumulating full buffers.for {var e errorfrag, e b.ReadSlice(delim)if e nil { // got final fragmentbreak}if e ! ErrBufferFull { // unexpected errorerr ebreak}// Make a copy of the buffer.buf : bytes.Clone(frag)fullBuffers append(fullBuffers, buf)totalLen len(buf)}totalLen len(frag)return fullBuffers, frag, totalLen, err
}这个方法初看起来还是蛮复杂的特别是它返回值有 4 个不过它这样的目的是为了调用者减少内存分配和拷贝。它的内部是依赖 ReadSlice 实现的如果直接读取到了那么就直接返回了。否则下面就是不同的地方了。通常如果返回 ErrBufferFull 是说明整个缓冲区没有遇到分隔符如果不是说明遇到了非预期错误直接返回它会保存已经读取的数据然后再调用 ReadSlice直到遇到分隔符或者其他错误。
对比上面这行代码的执行结果 ReadStringTest(CrazyDragonKingCrazyDragonKingCrazyDragonKing\nCrazyDragonKing\n)
总结
好了到这里就结束了。这里至少 Reader 相关的部分后面还会继续 Writer 相关的部分。不过目前还没有特别好的记录方式只是大致挑几个代码贴出来写一些自己的理解然后配一些示意图再加上一些测试代码。这篇博客不是为了说明这个包是怎么使用的而是为什么要用它以及它的一些实现的细节。现这样写吧以后再探索是不是有更好的方式。