Go netpoller
Go的并发模型
Go鼓励在goroutine中使用同步的编程方法,并创建多个goroutine,让它们之间通过channel通信。这种同步的编程方法大大方便了我们编写程序,不会出现callback满天飞的情况。
在之前的Go调度器的介绍中,我们已经知道,当在G(goroutine)中调用syscall时,不仅G会阻塞,相应的M(Thread)也会阻塞。这时go runtime会唤醒sleep状态的M,或创建新的M,来运行其他的G。这样就导致了一个问题:如果频繁调用会阻塞的syscall(例如对socket的fd执行阻塞的read),go runtime就会不断创建新的M(Thread),大量的Thread会耗光资源。
Go使用netpoller来避免这一问题。
netpoller
netpoller待在它自己的Thread中,从希望做Network I/O的goroutine中接收事件。
当你在Go中accept一个连接时,相应的fd会被设置O_NONBLOCK flag。若goroutine在读写该fd时遇到了EAGAIN,就会调用netpoller的wait方法,令netpoller在fd可读/写后通知该goroutine。之后,goroutine就会进入等待状态,不再参与调度。netpoller会用系统上的非阻塞网络I/O来监视该fd,在linux上是epoll,在BSD上是kqueue。
当netpoller从操作系统收到通知说该fd可以读/写时,会检查是否有goroutine被阻塞在该fd上。如果有,就通知它们。这样goroutine就可以重新执行之前的读/写操作。
所以实际上go使用的是非阻塞网络I/O,但是用了一套机制,令goroutine保持睡眠,直到socket变为可以读/写后,重新进行读/写。将异步变成了同步,简化了编程。
相关代码
net/fd_unix.go:
type netFD struct {
pfd poll.FD
// immutable until Close
family int
sotype int
isConnected bool
net string
laddr Addr
raddr Addr
}
func (fd *netFD) Read(p []byte) (n int, err error) {
n, err = fd.pfd.Read(p)
runtime.KeepAlive(fd)
return n, wrapSyscallError("read", err)
}internal/poll/fd_unix.go:
func (fd *FD) Read(p []byte) (int, error) {
...
for {
n, err := syscall.Read(fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
...
}
err = fd.eofError(n, err)
return n, err
}
}internal/poll/fd_poll_runtime.go:
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}
func (pd *pollDesc) wait(mode int, isFile bool) error {
if pd.runtimeCtx == 0 {
return errors.New("waiting for unsupported file type")
}
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}