最近在处理业务的时候遇到一个问题,在用使用io.pipe()的过程中,由于忘记pr.close()
而导致了内存泄漏,下面给出一个具体的场景例子,后面会逐步分析为什么忘记close会导致内存的泄漏。
package main
import (
"bytes"
"io"
"log"
"time"
)
func main() {
pr, pw := io.Pipe()
go func() {
defer func() { log.Print("Write close") }()
// First write
time.Sleep(time.Second)
buf := []byte{0, 1, 2, 3, 4, 5, 6, 7}
br := bytes.NewReader(buf)
_, err := io.Copy(pw, br)
if err != nil {
log.Printf("First write error: %v", err)
}
// Second write
time.Sleep(time.Second)
br.Reset(buf)
_, err = io.Copy(pw, br)
if err != nil {
log.Printf("Second write error: %v", err)
}
}()
result := make([]byte, 8)
pr.Read(result)
log.Println(result)
//pr.Close()
time.Sleep(5 * time.Second)
}
运行代码输出 2020/08/14 10:53:10 [0 1 2 3 4 5 6 7]
,表示goroutine并没有被关闭,导致内存泄漏。
(有人可能有疑问说return之后不就全关闭了么,怎么内存泄漏?但我这个只是一个test程序,如果运行在服务器上,当前的main函数也只是一个goroutine而已,所以仍然会导致内存泄漏)
io.Pipe
首先,我们通过查看io.Pipe的源码可以得知,io.pipe中使用wrCh和rdCh进行读写消息的传递。其中,wrCh用来传递真正的信息,rdCh用来传递reader读取的字符数量;而且因为是非缓冲区的channel,所以读写之间是强制同步的。同时,使用一个done的channel来表示这个pipe是否被关闭。
func Pipe() (*PipeReader, *PipeWriter) {
p := &pipe{
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}
return &PipeReader{p}, &PipeWriter{p}
}
在read的过程中,他首先会判断这个pipe有没有被close,如果没有就等待wrCh中的数据,更具select的原理我们可以知道,没有default的select会一直阻塞,所以这个read会一直阻塞,直到pipeWriter开始write(因为之前说过非缓冲的channel会强制同步)或者pipe被关闭
func (p *pipe) Read(b []byte) (n int, err error) {
select {
case <-p.done:
return 0, p.readCloseError()
default:
}
select {
case bw := <-p.wrCh:
nr := copy(b, bw)
p.rdCh <- nr
return nr, nil
case <-p.done:
return 0, p.readCloseError()
}
}
同样,write方法的代码也跟read方法相同,select部分会一直阻塞到pipeReader开始read或者pipe被关闭
func (p *pipe) Write(b []byte) (n int, err error) {
select {
case <-p.done:
return 0, p.writeCloseError()
default:
p.wrMu.Lock()
defer p.wrMu.Unlock()
}
for once := true; once || len(b) > 0; once = false {
select {
case p.wrCh <- b:
nw := <-p.rdCh
b = b[nw:]
n += nw
case <-p.done:
return n, p.writeCloseError()
}
}
return n, nil
}
问题原因
在本段代码中,如果不使用pr.close()
,第一次write和main函数中的read会强制同步而读写成功,但是当第二次进入write的时候,pipeWriter.write会因为无法向wrCh写入(强制同步)且done中无法读取数据(没有调用pr.close() )而阻塞在select中,导致goroutine持续无法关闭,从而产生内存泄漏,如果将pr.close()的注释去掉,则会输出:
2020/08/14 10:58:39 [0 1 2 3 4 5 6 7]
2020/08/14 10:58:40 Second write error: io: read/write on closed pipe
2020/08/14 10:58:40 Write close
pipe感知到关闭,从而报错退出阻塞。
io.Pipe如何关闭?
有一个问题,reader和writer在close的时候其实并没有向done里面传递信号,那么done为什么可以通知到write和read方法呢?比如read中有这样的代码:
select {
case bw := <-p.wrCh:
nr := copy(b, bw)
p.rdCh <- nr
return nr, nil
case <-p.done:
return 0, p.readCloseError()
}
read的close逻辑如下
func (p *pipe) CloseRead(err error) error {
if err == nil {
err = ErrClosedPipe
}
p.rerr.Store(err)
p.once.Do(func() { close(p.done) })
return nil
}
一开始的我比较不解,close(p.done)
跟 <-p.done
之间有什么关系呢?后来自己做了一个小实验才知道
func main() {
ch := make(chan int)
close(ch)
val := <-ch
log.Print(val)
val, ok := <-ch
log.Print(val, ok)
}
这个会输出什么呢?
2020/08/14 17:13:28 0
2020/08/14 17:13:28 0 false
所以其实一个channel关闭之后,也是可以从里面读取东西的,如果此时缓冲区中有东西,那么就会读取缓冲区中的东西,如果没有,就会返回默认值(此处为0)。避免这个的办法就是获取他的两个参数,第二个参数表示的就是通道是否被关闭,所以
- 如果channel没有被关闭且缓冲区没有值,那么读取动作会阻塞
- 如果channel已经关闭且缓冲区没有值,那么会读取一个默认值并给第二个参数赋值false
结论
对io.pipe的分析表明我对Golang的一些原理还不是很了解,记住两个结论
- 使用io.pipe的时候要将pr进行关闭,否则会导致内存泄漏
- 能从channel中取得值的时候不一定证明有值,也有可能是通道已经被关闭了