点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言
本文作者:鸟窝smallnest
原文链接:https://colobu.com/2019/07/21/concurrency-design-patterns-half-sync-half-async/
半同步/半异步(half-sync/half-async)的模式集成了同步 IO 模式和异步 IO 模型,既简化了并发系统的开发,又兼顾了效率。在这个并发设计模式的实现中,高级的任务使用同步 IO 模型,使开发者可以无太多的心智负担在并发编程中,底层的任务使用异步 IO 模型,这就提高了并发的效率。这个设计模式广泛的应用在操作系统的实现中,如 UNIX、Mach,Windows 等。
模式介绍
半同步/半异步的模式动机(Intent)就是从异步 IO 操作中解耦出同步 IO 操作,以便简化并发编程的效果,又不降低并发编程的效率。
下图是 BSD UNIX 网络子系统的架构图。BSD Unix 内核协调异步通信设备(如网络适配器和终端)和操作系统上运行的应用程序之间的 I/O。到达通信设备的数据包通过硬件中断异步化的中断处理器传送到操作系统内核。这些 handler 接收到这些来自设备包后会交给更高一层的协议去处理(IP、TCP 等)。合法的数据存在网络层的队列中。
BSD UNIX 网络子系统
操作系统会 dispatch 用户进程去消费这些数据。用户进程调用read
系统调用同步地从网络层读取数据,如果没有可用的数据,进程会 sleep 直到有新的数据到来。
在 BSD 架构中,对于设备的中断信号,内核异步地执行 IO 操作,而用户进程则同步地执行,这就是一个典型的半同步/半异步
模式的实现。
半同步/半异步模式的参与者
当出现下面的场景的时候,你可能需要考虑采用半同步/半异步设计模式:
- 系统需要异步地处理外部事件
- 为每一个外部事件指定一个单独的控制线程来执行 IO 操作是低效的
- 如果同步 IO,可以显著简化系统中的高级任务
如上图所示,这个设计模式包含一些参与者:
- Synchronous task layer:如上面的例子中的用户进程
- Queueing layer:如上面的例子中的 Socket 层
- Asynchronous task layer:如上面的例子中的 BSD UNIX 内核
- External event sources:如上面的例子中的网络设备
简单例子
在上一个并发设计模式Active Object[1], 我们介绍 Active Object 提供了异步访问接口,并且我们可以很容易地的把异步访问接口封装成同步调用接口(Call
-> Do
, 或者调用future.get
等方式)。
一旦调用者采用同步的接口去调用 Active Object,那么调用者、Active Object 整个过程就组成了半同步/半异步的方式,也就是说,我们可以使用 Active Object 来实现半同步/半异步并发设计模式。
但是半同步/半异步设计模式中的半异步
不一定非要 Active Object 来实现,你也可以使用消息模式来实现。这是这两种设计模式的联系和区别。
实际案例
Go 中的网络库就是通过这个模式来实现的。
我们知道,Go 大大简化了并发编程的复杂性,通过传统的我们所熟悉的同步的编程方式,我们就实现异步的编程方式。
为了高效地处理底层的网络 IO,GO 采用多路复用的方式(linux epoll/windows iocp/freebsd,darwin kqueue/solaris Event Port)处理网络 IO 事件,并且提供一致的同步的net.Conn
对象来实现同步的调用。
所以很多同学曾经询问,怎么在 Go 中实现 epoll 方式处理网络 IO 呢?本身 Go 将这些异步事件(epoll 等事件)处理封装在底层,你是无法直接调用的,并且 Go 的这种方式在大部分场景下已经很高效了,除非是在很特别的场景下,你才会去处理底层的 IO,比如使用evio[2]、epoller[3]等库,但是使用起来是比较复杂的,而且风险性也很高。
以下是 Go 实现半同步/半异步模式相关的一些文件。
- runtime/netpoll.go[4]
- runtime/netpoll_epoll.go[5]
- runtime/proc.go[6]
- net/fd_unix.go[7]
- internal/poll/fd.go[8]
- internal/poll/fd_poll_runtime.go[9]
- internal/poll/fd_unix.go[10]
这里不详细介绍 Go 网络层的具体实现了,国内一些开发者已经不断地在挖掘和介绍相关的内容了,你可以阅读参考资料中的一些链接去了解 Go 的具体实现。
基本上, runtime/netpoll.go
中定义了一个通用的poll
接口,如poll_runtime_pollOpen
、poll_runtime_pollClose
等。这是一个了不起的操作,一位内不同的操作系统有不同的实现(epoll/kqueue/iocp 等),而且它们的实现方式和方法也不统一,所以能封装成一个统一的模型已经很了不起了。针对不同的操作系统它有不同的文件和实现。
runtime/proc.go
是 Go 调度器的核心,它会调度使用 netpoll 的 goroutine。
net/fd_unix.go
中定义的netFD
是 Go 网络库的核心。它基本类似 Active Object 模式中的Proxy
,对调用者来说它提供了同步的方法,内部使用统一 poll 模型实现多路复用。
我们可以观察Read
方法,看看它是如果实现同步转异步的。
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)
}
它的Read
方法实际调用poll.FD
的Read
方法。poll.FD
定义在internal/poll/fd.go
中(针对 unix 类型的架构,实际在internal/poll/fd_unix.go
中)。
func (fd *FD) Read(p []byte) (int, error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if len(p) == 0 {
// If the caller wanted a zero byte read, return immediately
// without trying (but after acquiring the readLock).
// Otherwise syscall.Read returns 0, nil which looks like
// io.EOF.
// TODO(bradfitz): make it wait for readability? (Issue 15735)
return 0, nil
}
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
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
}
}
// On MacOS we can see EINTR here if the user
// pressed ^Z. See issue #22838.
if runtime.GOOS == "darwin" && err == syscall.EINTR {
continue
}
}
err = fd.eofError(n, err)
return n, err
}
}
这个方法可以从网络 IO 中读取p []byte
字节数组,或者出错,或者没有数据。因为它是通过epoll
(linux)实现的,操作系统可以将数据是否可读等 epoll 事件通知这个底层实现,这个底层实现可以检查特定的标志(err, syscall.EAGAIN, fd.pd.pollable())来决定是否等待读取。
整体来看,Go 屏蔽了底层的异步处理,为各个操作系统平台提供了统一的同步调用接口,简化了网络处理的逻辑,同时还保留了多路复用的异步处理的性能。
参考资料
- https://www.dre.vanderbilt.edu/~schmidt/PDF/PLoP-95.pdf
- http://likakuli.com/post/2018/06/06/golang-network/
- https://zhuanlan.zhihu.com/p/31644462
文中链接
[1]Active Object: https://colobu.com/2019/07/02/concurrency-design-patterns-active-object/
[2]evio: https://github.com/tidwall/evio
[3]epoller: https://github.com/smallnest/epoller
[4]runtime/netpoll.go: https://github.com/golang/go/blob/master/src/runtime/netpoll.go
[5]runtime/netpoll_epoll.go: https://github.com/golang/go/blob/master/src/runtime/netpoll_epoll.go
[6]runtime/proc.go: https://github.com/golang/go/blob/master/src/runtime/proc.go
[7]net/fd_unix.go: https://github.com/golang/go/blob/master/src/net/fd_unix.go
[8]internal/poll/fd.go: https://github.com/golang/go/blob/master/src/internal/poll/fd.go
[9]internal/poll/fd_poll_runtime.go: https://github.com/golang/go/blob/master/src/internal/poll/fd_poll_runtime.go
[10]internal/poll/fd_unix.go: https://github.com/golang/go/blob/master/src/internal/poll/fd_unix.go
推荐阅读
Go并发设计模式之 Active Object
喜欢本文的朋友,欢迎关注“Go语言中文网”:
Go语言中文网启用微信学习交流群,欢迎加微信:274768166