缓冲区的基本概念

缓冲区是在内存中分配的一段空间,用于暂时存储数据。当数据从一个设备传送到另一个设备时,缓冲区可以用来平滑这种传输过程,使得数据的发送和接收更加高效。

为什么需要缓冲区

背景:
计算机磁盘是由一个个磁道构成,每次进行数据读写的时候,都需要通过磁头寻找到到合适的磁道,才能执行读写数据的操作,而这个过程需要耗费一定的时间,即寻道时间

原理:
当我们引入缓冲区后,可以将多次小量的数据写入操作合并为一次大量的写入操作,从而大大减少寻道时间和旋转延迟,提高数据的读写效率。

与缓存的区别:

缓存主要用于存储最近或最经常访问的数据,利用硬件的性能优势,加快数据的访问速度,例如redis缓存利用内存速度远远大于磁盘的优势。在计算机底层则有CPU缓存(L1、L2、L3);内存缓存DRAM Cache;磁盘缓存Disk Cache等,从磁盘到CPU的缓存空间越来越大,性能也越来越好,造价也越来越高

而缓冲区主要用于减少数据传输和磁盘的读写次数,提高数据的读写效率。

缓冲区类型

  1. 全缓冲:数据只有在缓冲区满时才会被实际写入或读取。
  2. 行缓冲:在文本输入的情况下,数据在遇到换行符时会被实际写入或读取。
  3. 无缓冲:每次数据写入或读取都会立即执行。

自行实现带缓冲的I/O

用go代码作为演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type BufferedFileWriter struct {
buffer []byte
bufferEndIndex int
fout *os.File
}

func NewWriter(f *os.File, bufferSize int) *BufferedFileWriter {
return &BufferedFileWriter{
buffer: make([]byte, bufferSize),
bufferEndIndex: 0,
fout: f,
}
}

func (w *BufferedFileWriter) Write(p []byte) {
if len(p) >= len(w.buffer) {
w.flush()
_, _ = w.fout.Write(p)
} else {
if w.bufferEndIndex+len(p) >= len(w.buffer) {
w.flush()
}
copy(w.buffer[w.bufferEndIndex:], p)
w.bufferEndIndex += len(p)
}
}

func (w *BufferedFileWriter) flush() {
_, _ = w.fout.Write(w.buffer[0:w.bufferEndIndex])
w.bufferEndIndex = 0
}

func (w *BufferedFileWriter) WriteString(s string) {
w.Write([]byte(s))
}

与直接写文件和标准库的bufio对比耗时
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//直接写文件
func WriteDirectly() {
fout, err := os.OpenFile(outFile1, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
HandleError(err)
}
defer func(fout *os.File) {
if err := fout.Close(); err != nil {
HandleError(err)
}
}(fout)

for i := 0; i < 9999; i++ {
if _, err := fout.WriteString(text + "\n"); err != nil {
HandleError(err)
}
}
}

//带缓冲区写文件
func WriteWithBuffer() {
fout, err := os.OpenFile(outFile2, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
HandleError(err)
}
defer func(fout *os.File) {
if err := fout.Close(); err != nil {
HandleError(err)
}
}(fout)

// 定义较大的缓冲区减少写磁盘次数,空间换时间
writer := NewWriter(fout, 8*1024)
defer writer.flush()
for i := 0; i < 9999; i++ {
writer.WriteString(text + "\n")
}
}

// bufio写文件
func WriteWithBufio() {
fout, err := os.Create(outFile3)
if err != nil {
HandleError(err)
}

defer func(fout *os.File) {
if err := fout.Close(); err != nil {
HandleError(err)
}
}(fout)
writer := bufio.NewWriter(fout)
defer writer.Flush()
for i := 0; i < 9999; i++ {
if _, err := writer.WriteString(text + "\n"); err != nil {
HandleError(err)
}
}
}

func TestBufferIO(t *testing.T) {
t1 := time.Now()
WriteDirectly()
t2 := time.Now()
WriteWithBuffer()
t3 := time.Now()
WriteWithBufio()
t4 := time.Now()
fmt.Printf("直接写文件耗时:%v, 自行实现带缓冲区写文件:%v, 使用go标准库bufio写文件耗时:%v", t2.Sub(t1), t3.Sub(t2), t4.Sub(t3))
}

结果:

可以看出,自行实现带缓冲区写文件比直接写文件要快很多,速度接近于使用go标准库的bufio。