Golang 阻塞和非阻塞

Posted

技术标签:

【中文标题】Golang 阻塞和非阻塞【英文标题】:Golang blocking and non blocking 【发布时间】:2016-07-06 20:44:37 【问题描述】:

我对 Go 如何处理非阻塞 IO 感到有些困惑。 API 对我来说大多是同步的,在 Go 上观看演示文稿时,听到诸如“和调用块”之类的 cmets 并不少见

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

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

所以 TLDR; 在 Go 例程中执行 IO 时,Go 会阻塞当前线程,还是会使用延续将其转换为 C# 之类的异步等待状态机?

【问题讨论】:

【参考方案1】:

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

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

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

https://morsmachine.dk/go-scheduler

【讨论】:

我要补充一点,Go 运行时调度程序当前(Go 1.6 及更低版本)仅多路复用(Linux 上的 epoll,Windows 上的 IOCP 等)仅网络 I/O 系统调用。所有命中磁盘、串行等的 I/O 系统调用每个都占用一个 OS 线程。这在 Go 开发者社区中是好是坏是有争议的。当前的共识似乎是让用户可以使用一般的异步 I/O 会很好,但从实际的角度来看,它并不是真的有用... ...如——如果你有 1000 个 goroutine 同时写入同一个磁盘驱动器,异步 I/O 不会真正有帮助;使用专用写入器和缓冲通道。附带说明:公开底层操作系统的异步/轮询接口的第 3 方包确实存在。 我发现关于file io epoll 的讨论。 github.com/golang/go/issues/18507,还有另一个公关github.com/golang/go/commit/…。我认为两篇文章将解决您关于no blocking io on file and network, when golang makes thread blocking?的问题【参考方案2】:

您应该首先阅读@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 上执行,这基本上是一个无限循环,检查应该执行的回调队列。甚至可以自己实现,真的很简单。

【讨论】:

我认为这里的“块”这个词存在语义问题,如果 Go 例程产生并且可以稍后被唤醒,那么代码中必须有一些东西可以使它工作,例如延续传球风格或类似的东西。不?所以它表现得好像它正在阻塞,但在幕后它产生执行,然后被唤醒并继续?我假设如果我在 Go 例程中有一个永无止境的 for 循环,那么 Go 例程将永远不会退出,并且当前运行 Go 例程的线程将永远被阻塞,对吧?如果不是这样,那么我在这里完全感到困惑。 您应该先阅读@Not_a_Golfer 的答案以及他提供的链接,以了解 goroutines 的调度方式。我的回答更像是专门研究网络 IO 的北斗七星。是的,“块”的含义取决于上下文。从程序员的角度来看,它确实会阻塞。您的代码阻塞并且在调用返回之前不会继续。从运行时的角度来看,它产生了执行。这就是我称之为停车的原因——这是围棋中使用的一个真实术语。它的协作多任务和无限循环确实会永远阻塞 goroutine 和操作系统线程,因为它永远不会产生执行。 @RogerAlsing 是的,如果一个 goroutine 从不做任何“阻塞”的事情,并且从不调用 runtime.Gosched(这是一个显式的调度程序 yield),它将无限期地占用它的 P,阻止其他 goroutine 运行它。 @RogerAlsing 了解有关该主题的更多信息,***.com/questions/35471480/… 请解释一下-1。我知道我的回答可能会让不知道 Go 内部工作原理的人感到困惑。但我并不打算解释一切。我特别选择了实现方式非常不同的网络 IO。【参考方案3】:

有一些issuespull request 可能对你有帮助:)

它可能会解决一些问题,例如

    golang 什么时候会阻塞 IO 操作?

    为什么 golang 只使用 async io 代替 socket 而不是 normal file

    https://github.com/golang/go/issues/18507 https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac https://github.com/golang/go/issues/6222 https://github.com/golang/go/issues/6817 Epoll on regular files

【讨论】:

以上是关于Golang 阻塞和非阻塞的主要内容,如果未能解决你的问题,请参考以下文章

Golang✔️走进 Go 语言✔️ 第十七课 select & 超时和非阻塞

阻塞赋值和非阻塞赋值有何区别

verilog 阻塞和非阻塞啥区别啊?

Linux 阻塞和非阻塞IO 实验

同步异步阻塞和非阻塞

阻塞赋值和非阻塞赋值