我对Go如何处理非阻塞IO感到有些困惑。 API对我来说大多看起来都是同步的,当在Go上观看演示时,听到诸如"以及呼叫块"
之类的评论并不罕见从文件或网络读取时,Go是否使用阻止IO? 或者是否有某种魔法在Go Goout内部使用时重写代码?
来自C#背景,这感觉非常不直观,在C#中,我们在使用异步API时拥有await
关键字。
这清楚地表明API可以产生当前线程并在稍后的延续中继续。
所以TLDR; 在Go例程中执行IO时,Go会阻塞当前线程,还是会使用continuation将其转换为类似异步等待状态机的C#?
答案 0 :(得分:30)
Go有一个调度程序,可以让您编写同步代码,并自行进行上下文切换,并使用异步IO。因此,如果您运行多个goroutine,它们可能在单个系统线程上运行,并且当您的代码在goroutine的视图中阻塞时,它并不是真正阻塞。这不是魔术,但是,它掩盖了你所有这些东西。
调度程序将在需要时分配系统线程,并在真正阻塞的操作期间分配系统线程(我认为文件IO例如阻塞或调用C代码)。但是,如果你正在做一些简单的http服务器,你可以使用实际上少量的“真实线程”来成千上万的goroutine。
您可以在此处详细了解Go的内部工作原理:
答案 1 :(得分:18)
您应首先阅读@Not_a_Golfer答案以及他提供的链接,以了解如何安排goroutine。我的答案更像是北斗星潜入网络IO。我假设您了解Go如何实现协作式多任务处理。
Go可以并且确实只使用阻塞调用,因为一切都在goroutines中运行,而且它们不是真正的操作系统线程。他们是绿线。因此,你可以让其中许多都阻塞IO调用,它们不会像OS线程一样占用你的所有内存和CPU。
文件IO只是系统调用。 Not_a_Golfer已经涵盖了这一点。 Go将使用真实OS线程在系统调用上等待,并在返回时取消阻止goroutine。 Here您可以看到Unix的文件read
实现。
网络IO不同。运行时使用“网络轮询器”来确定哪个goroutine应该从IO调用中解除阻塞。根据目标操作系统,它将使用可用的异步API来等待网络IO事件。调用看起来像阻塞但内部一切都是异步完成的。
例如,当您在TCP套接字goroutine上调用read
时,首先会尝试使用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
here的netpoll
实施。
对于C#中的异步/等待。异步网络IO也将使用异步API(Windows上的IO完成端口)。当某些东西到来时,操作系统将在其中一个线程池的完成端口线程上执行回调,这将继续当前的SynchronizationContext
。从某种意义上说,有一些相似之处(停车/停车确实看起来像调用延续,但在更低的层次上),但这些模型是非常不同的,更不用说实现了。默认情况下,Goroutines不绑定到特定的OS线程,它们可以在其中任何一个上恢复,这没关系。没有UI线程可以处理。 Async / await专门用于使用SynchronizationContext
在同一OS线程上恢复工作。并且因为没有绿色线程或单独的调度程序async / await必须将您的函数拆分为多个回调,这些回调在SynchronizationContext
上执行,这基本上是一个无限循环,它检查应该执行的回调队列。你甚至可以自己实现它,这很容易。
答案 2 :(得分:0)
有些issues
和pull request
可能会帮助您:)
它也许可以解决一些问题,例如
为什么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
答案 3 :(得分:-1)
Go会在执行IO或系统调用时阻止当前的goroutine,但是当发生这种情况时,允许运行另一个goroutine而不是阻塞的goroutine。较旧版本的Go一次只允许一个正在运行的goroutine,但从1.5开始,该数字已更改为可用的cpu核心数。 (runtime.GOMAXPROCS)
您不必担心Go中的阻止。例如,标准库的http服务器在goroutine中执行处理函数。如果您在提供http请求时尝试读取文件,则会阻止,但如果在第一个请求被阻止时进入另一个请求,则将允许另一个goroutine运行并提供该请求。然后当第二个goroutine完成,并且第一个goroutine不再被阻塞时,它将被恢复(如果GOMAXPROCS> 1,如果有一个自由线程,则阻塞的goroutine可能会更早恢复)。
有关详细信息,请查看以下内容: