Goroutines vs asyncio 任务 + CPU-bound 调用的线程池
Posted
技术标签:
【中文标题】Goroutines vs asyncio 任务 + CPU-bound 调用的线程池【英文标题】:Goroutines vs asyncio tasks + thread pool for CPU-bound calls 【发布时间】:2017-05-30 22:34:33 【问题描述】:goroutines 是否大致等同于 python 的 asyncio
任务,还有一个附加特性是任何 CPU 绑定任务都被路由到 ThreadPoolExecutor
而不是添加到事件循环中(当然,假设我们使用没有 GIL 的 python 解释器)?
我缺少的两种方法之间有什么实质性区别吗?当然,除了并发是 Go 不可或缺的一部分所带来的效率和代码清晰度之外。
【问题讨论】:
“任何受 CPU 限制的任务都被路由到它自己的线程”这不是 goroutines 的工作方式,所以我不确定该部分的问题是什么。 我认为 go runtime 会动态更改线程数以确保 goroutine 不会相互阻塞。由于事件循环已经可以在不阻塞的情况下处理 I/O 密集型任务,但不能处理 CPU 密集型任务,我得出的结论是,等效的处理方法是将 CPU 密集型任务放在它们自己的线程中。也许我应该说“路由到线程池”而不是“路由到自己的线程”?我编辑了这个问题。 【参考方案1】:我想我知道部分答案。我试图总结我对asyncio
任务和goroutines之间差异的理解,按重要性排序:
1) 与asyncio
不同,人们很少需要担心他们的 goroutine 会阻塞太久。 OTOH,goroutine 之间的内存共享类似于线程之间的内存共享,而不是 asyncio
任务,因为 goroutine 执行顺序保证要弱得多(即使硬件只有一个核心)。
asyncio
只会在显式的await
、yield
和某些事件循环方法上切换上下文,而 Go 运行时可能会开启更微妙的触发器(例如某些函数调用)。所以asyncio
是完全合作的,而 goroutine 只是大部分合作(路线图表明随着时间的推移它们会变得更不合作)。
一个非常紧密的循环(例如数字计算)仍然可能阻塞 Go 运行时(嗯,它正在运行的线程)。如果发生这种情况,它的影响将比在 python 中小——除非它发生在多个线程中。
2) Goroutines 对并行计算有现成的支持,这需要asyncio
下的更复杂的方法。
Go 运行时可以并行运行线程(如果有多个内核可用),因此它有点类似于在无 GIL 的 Python 运行时下的线程池中运行多个 asyncio
事件循环,并带有语言感知负载均衡器在前面。
3) Go 运行时会在单独的线程中自动处理阻塞的系统调用;这需要在asyncio
下明确完成(例如,使用run_in_executor
)。
也就是说,就内存成本而言,goroutine 非常类似于 asyncio
任务而不是线程。
【讨论】:
【参考方案2】:我想你可以认为它在下面是这样工作的,当然。这不是很准确,但足够接近。
但是有一个很大的不同:在 Go 中你可以编写直线代码,所有的 I/O 阻塞都会自动为你处理。您可以用简单的直线代码调用 Read,然后 Write,然后 Read。据我了解,使用 Python asyncio,您需要排队一个函数来处理读取,而不仅仅是调用 Read。
【讨论】:
谢谢!我的自我回答是否有任何重大误解?是的,在 python 中,您要么需要使用异步 IO 库,要么如果它不可用,则需要将 IO 操作显式发送到线程池(这可以通过返回未来来简化,这样它就不会中断异步代码风格)。对同步阻塞 IO 的天真调用会挂起事件循环。 Go 运行时确实会根据需要启动新线程。在将 goroutine 与异步 I/O 函数进行比较时,我不确定该说什么。它们确实不同,所以当你说这两种方法是等价的时,我会说只有在非常松散的解释下才是正确的。 不,在 python asyncio 中,您可以执行类似await stream_reader.read(num_bytes)
的操作,这与阻塞世界中的 file.read(num_bytes
并没有太大区别以上是关于Goroutines vs asyncio 任务 + CPU-bound 调用的线程池的主要内容,如果未能解决你的问题,请参考以下文章
Python 3.6 asyncio - 从未检索到任务异常 - 任务的产量不好:200