Golang 阻塞与非阻塞

Golang blocking and non blocking

我对 Go 如何处理非阻塞 IO 感到有些困惑。 API 对我来说大部分看起来是同步的,并且在观看关于 Go 的演示时,听到像 "and the call blocks"

这样的评论并不少见

Go 在从文件或网络读取时是否使用阻塞 IO? 或者是否有某种魔法可以在 Go Routine 内部使用时重写代码?

来自 C# 背景,这感觉非常不直观,在 C# 中,我们在使用异步 API 时有 await 关键字。 这清楚地表明 API 可以产生当前线程并稍后在延续中继续。

所以 TLDR; Go 会在 Go 例程中执行 IO 时阻塞当前线程,还是会使用 continuations 将其转换为类似 C# 的 async await 状态机?

Go 有一个调度程序,可以让你编写同步代码,并自行进行上下文切换,并在后台使用异步 IO。因此,如果您 运行 多个 goroutines,它们可能 运行 在单个系统线程上,并且当您的代码从 goroutine 的视图中阻塞时,它并不是真正的阻塞。这不是魔法,但是是的,它掩盖了你所有这些东西。

调度程序将在需要时分配系统线程,并在真正阻塞的操作期间分配系统线程(例如,我认为文件 IO 正在阻塞,或调用 C 代码)。但是如果你正在做一些简单的 http 服务器,你可以有成千上万的 goroutine 使用实际上少数 "real threads"。

您可以在此处阅读有关 Go 内部工作原理的更多信息:

https://morsmachine.dk/go-scheduler

您应该先阅读@Not_a_Golfer 的回答和他提供的 link 以了解 goroutine 的调度方式。我的回答更像是对网络 IO 的更深入研究。我假设您了解 Go 如何实现协作式多任务处理。

Go 可以而且确实只使用阻塞调用,因为一切都在 goroutine 中运行,它们不是真正的 OS 线程。它们是绿色的线。所以你可以让它们中的许多都阻塞在 IO 调用上,它们不会吃掉你所有的内存并且 CPU 就像 OS 线程那样。

文件 IO 只是系统调用。 Not_a_Golfer 已经介绍过了。 Go 将使用真正的 OS 线程等待系统调用,并在 return 时解除 goroutine 的阻塞。 Here 您可以查看文件 read Unix 实现。

网络IO不同。运行时使用 "network poller" 来确定哪个 goroutine 应该从 IO 调用中解除阻塞。根据目标 OS 它将使用可用的异步 API 来等待网络 IO 事件。调用看起来像阻塞,但内部一切都是异步完成的。

例如,当您在 TCP 套接字 goroutine 上调用 read 时,首先会尝试使用系统调用进行读取。如果什么都没有到达,它将阻塞并等待它被恢复。在这里阻塞是指将 goroutine 放入等待恢复的队列中。这就是当你使用网络 IO 时 "blocked" goroutine 将执行让给其他 goroutines 的方式。

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

当数据到达时,网络轮询器将 return 应该恢复的 goroutines。可以看到here findrunnable function that searches for goroutines that can be run. It calls netpoll function which will return goroutines that can be resumed. You can find kqueue implementation of netpoll here.

至于 C# 中的 async/wait。异步网络 IO 也将使用异步 API(Windows 上的 IO 完成端口)。当某些东西到达时 OS 将在线程池的完成端口线程之一上执行回调,这将继续当前 SynchronizationContext。从某种意义上说,有一些相似之处(parking/unparking 确实看起来像调用延续,但级别要低得多)但是这些模型非常不同,更不用说实现了。默认情况下,goroutines 不绑定到特定的 OS 线程,它们可以在其中任何一个上恢复,这无关紧要。没有 UI 个线程需要处理。 Async/await 专门用于使用 SynchronizationContext 在同一个 OS 线程上恢复工作。而且因为没有绿色线程或单独的调度程序 async/await 必须将您的函数拆分为多个回调,这些回调在 SynchronizationContext 上执行,这基本上是一个无限循环,用于检查应该执行的回调队列.你甚至可以自己实现它,真的很简单。