您应该首先阅读@Not_a_Golfer 的答案以及他提供的链接,以了解 goroutines 的调度方式。我的回答更像是专门深入研究网络 IO。我假设你了解 Go 如何实现协作式多任务处理。
Go 可以并且确实只使用阻塞调用,因为一切都在 goroutines 中运行,它们不是真正的操作系统线程。它们是绿色的线。因此,您可以让它们中的许多都阻塞 IO 调用,它们不会像操作系统线程那样占用您所有的内存和 CPU。
文件 IO 只是系统调用。 Not_a_Golfer 已经涵盖了这一点。 Go 将使用真正的操作系统线程来等待系统调用,并在 goroutine 返回时解除阻塞。 Here 你可以看到文件read Unix 的实现。
网络 IO 不同。运行时使用“网络轮询器”来确定哪个 goroutine 应该从 IO 调用中解除阻塞。根据目标操作系统,它将使用可用的异步 API 来等待网络 IO 事件。调用看起来像阻塞,但内部一切都是异步完成的。
例如,当您在 TCP 套接字上调用 read 时,goroutine 首先将尝试使用 syscall 进行读取。如果什么都没有到达,它将阻塞并等待它恢复。在这里阻塞是指停车,它将 goroutine 置于等待恢复的队列中。这就是当你使用网络 IO 时“阻塞”的 goroutine 将执行权交给其他 goroutine 的方式。
func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if err := fd.pd.PrepareRead(); err != nil {
return 0, err
}
for {
n, err = syscall.Read(fd.sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN {
if err = fd.pd.WaitRead(); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
break
}
if _, ok := err.(syscall.Errno); ok {
err = os.NewSyscallError("read", err)
}
return
}
https://golang.org/src/net/fd_unix.go?s=#L237
当数据到达时,网络轮询器将返回应该恢复的 goroutine。你可以看到 here findrunnable 搜索可以运行的 goroutine 的函数。它调用netpoll 函数,该函数将返回可以恢复的goroutine。你可以找到kqueue实现netpollhere。
至于 C# 中的异步/等待。异步网络 IO 也将使用异步 API(Windows 上的 IO 完成端口)。当某些东西到达时,操作系统将在线程池的完成端口线程之一上执行回调,这将继续当前的SynchronizationContext。从某种意义上说,有一些相似之处(停车/取消停车看起来确实像调用延续,但在较低级别上)但这些模型非常不同,更不用说实现了。默认情况下,Goroutines 不绑定到特定的 OS 线程,它们可以在其中任何一个上恢复,没关系。没有 UI 线程需要处理。 Async/await 专门用于使用SynchronizationContext 在同一 OS 线程上恢复工作。而且因为没有绿色线程或单独的调度程序 async/await 必须将您的函数拆分为多个回调,这些回调在 SynchronizationContext 上执行,这基本上是一个无限循环,检查应该执行的回调队列。甚至可以自己实现,真的很简单。