最近在处理业务的时候遇到一个问题,在用使用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如何关闭?

golang可以做python的第三方库么 golang io.pipe_golang

有一个问题,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中取得值的时候不一定证明有值,也有可能是通道已经被关闭了