我对Go语言如何处理非阻塞I/O感到困惑。在我看来,Go语言的API大多数是同步的,在Go语言上观看演示时,经常会听到“和调用阻塞”这样的评论。
Go语言在阅读文件或网络时是否使用了阻塞I/O,或者是否有某种魔力可以在goroutine内部重写代码?
从C#背景来看,这感觉非常不直观,因为在C#中,我们在使用异步API时使用了await
关键字,它清楚地传达了API可以生成当前线程并在后续的continuation中继续。
TLDR; Go语言在goroutine?中执行I/O时会阻塞当前线程吗?还是会使用延续将其转换为类似C#的async/await状态机?
2条答案
按热度按时间bvn4nwqk1#
Go语言有一个调度器,它可以让你编写同步代码,并且可以自己进行上下文切换,还可以在后台使用异步I/O。所以如果你运行多个goroutine,它们可能会在一个系统线程上运行,当你的代码在goroutine的视角下阻塞时,它并不是真正的阻塞。这并不神奇,但是是的,它屏蔽了所有这些东西。
调度器会在需要的时候分配系统线程,在真正阻塞的操作(例如文件I/O阻塞,或者调用C代码)中分配线程,但是如果你在做一些简单的http服务器,你可以有成千上万的goroutine使用实际上是少数的“真正的线程”。
您可以阅读更多有关Go语言here内部工作原理的内容。
i86rm4rw2#
你应该先阅读@Not_a_Golfer的答案和他提供的链接,了解goroutine是如何被调度的。我的答案更像是对网络IO的深入研究。我想你应该了解Go语言是如何实现协作多任务的。
Go语言可以也确实只使用阻塞调用,因为所有的东西都在goroutine中运行,它们不是真实的的操作系统线程,而是绿色线程,所以你可以让很多阻塞调用都阻塞IO调用,它们不会像操作系统线程那样消耗你所有的内存和CPU。
文件IO只是系统调用。Not_a_Golfer已经介绍过了。Go语言将使用真实的的操作系统线程来等待系统调用,并在它返回时解除对goroutine的阻塞。这里你可以看到Unix下的
read
文件实现。网络IO是不同的。运行时使用“网络轮询器”来决定哪个goroutine应该从IO调用中解除阻塞。根据目标操作系统的不同,它将使用可用的异步API来等待网络IO事件。调用看起来像阻塞,但在内部一切都是异步完成的。
例如,当你在TCP套接字上调用
read
时,goroutine首先会尝试使用syscall进行读取,如果还没有任何消息到达,它会阻塞并等待恢复。这里的阻塞是指将goroutine放入一个等待恢复的队列中。这就是当你使用网络IO时,被阻塞的goroutine将执行权让给其他goroutine的方式。https://golang.org/src/net/fd_unix.go?s=#L237
当数据到达时,网络轮询器会返回需要恢复的goroutine。这里可以看到
findrunnable
函数,它搜索可以运行的goroutine。它调用netpoll
函数,返回可以恢复的goroutine。这里可以看到netpoll
的kqueue
实现。对于C#中异步/等待,异步网络IO也将使用异步API(Windows上的IO完成端口)。当有东西到达时,操作系统将在线程池的完成端口线程之一上执行回调,这将在当前
SynchronizationContext
上放置continuation。(停车/不停车看起来确实像呼叫延续,但是在低得多的级别上)但是这些模型非常不同,更不用说实现了。2默认情况下,goroutine并不绑定到一个特定的操作系统线程,它们可以在任何一个线程上恢复,这没关系。没有UI线程需要处理。异步/等待是专门为使用SynchronizationContext
恢复同一OS线程上的工作而设计的。而且因为有'没有绿色线程或者一个单独的调度程序async/await必须把你的函数分成多个回调函数,这些回调函数在SynchronizationContext
上执行,这基本上是一个无限循环,检查应该执行的回调函数队列。你甚至可以自己实现它,这真的很容易。