Multithreading 戈朗阻塞与非阻塞

Multithreading 戈朗阻塞与非阻塞,multithreading,go,nonblocking,Multithreading,Go,Nonblocking,我对Go如何处理非阻塞IO有些困惑。 API在我看来大多是同步的,在观看演示文稿时,经常听到诸如“和调用块”之类的评论 Go在从文件或网络读取时是否使用阻塞IO? 或者,当在Go例程中使用时,是否存在某种重新编写代码的魔法 来自C#背景,这感觉非常不直观,在C#中,在使用异步API时,我们有wait关键字。 这清楚地传达了API可以生成当前线程并在后续中继续 因此,TLDR; 在Go例程中执行IO时,Go会阻止当前线程,还是会使用continuations将其转换为类似C的异步等待状态机?Go有

我对Go如何处理非阻塞IO有些困惑。 API在我看来大多是同步的,在观看演示文稿时,经常听到诸如“和调用块”之类的评论

Go在从文件或网络读取时是否使用阻塞IO? 或者,当在Go例程中使用时,是否存在某种重新编写代码的魔法

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

因此,TLDR;
在Go例程中执行IO时,Go会阻止当前线程,还是会使用continuations将其转换为类似C的异步等待状态机?

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

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

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


你应该先阅读@Not_a_Golfer answer和他提供的链接,了解goroutines是如何安排的。我的回答更像是对网络IO的深入研究。我想你知道Go是如何实现多任务协作的

Go可以而且确实只使用阻塞调用,因为所有东西都在Goroutine中运行,它们不是真正的OS线程。它们是绿色的线。因此,您可以让它们中的许多都阻塞IO调用,它们不会像操作系统线程那样占用您所有的内存和CPU

文件IO只是系统调用。不是一个高尔夫球手已经讲过了。Go将使用real OS线程等待系统调用,并在goroutine返回时解除阻止。您可以看到Unix的文件
read
实现

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

例如,当您在TCP套接字上调用
read
时,goroutine first将尝试使用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
}

当数据到达时,网络轮询器将返回应该恢复的goroutines。您可以看到搜索可以运行的goroutine的
findrunnable
函数。它调用
netpoll
函数,该函数将返回可以恢复的goroutines。您可以找到
kqueue
netpoll
实现


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

存在一些
问题
,而
拉取请求可能会对您有所帮助:)

它也许能解决一些问题,比如

  • golang将在何时阻止IO操作
  • 为什么golang只对
    套接字使用
    异步io
    ,而不是
    普通文件


  • 我要补充的是,Go运行时调度器当前(Go 1.6及以下版本)只复用(Linux上的epoll、Windows上的IOCP等)网络I/O系统调用。所有命中磁盘、串行等的I/O系统调用都占用一个操作系统线程。这是好是坏在Go开发者社区是有争议的。目前的共识似乎是,让用户可以使用通用异步I/O是很好的,但从实用的角度来看,它并没有那么有用……比如——如果有1000个goroutine同时写入同一个磁盘驱动器,异步I/O就没有什么帮助;使用专用写入程序和缓冲通道。附带说明:公开底层操作系统异步/轮询器接口的第三方软件包确实存在。我发现了关于
    文件io epoll
    的讨论,还有另外一个公关。我认为两篇帖子可以解决你的问题,即当golang进行线程阻塞时,文件和网络上没有阻塞io?
    我认为“阻塞”这个词在语义上有问题,如果Go例程产生,并且可以稍后唤醒,那么代码中一定有某种东西使其工作,例如,连续传球的风格或类似的东西。不因此,它的行为就好像是在阻挡,但在幕后它是在阻止