我啥时候应该在常规线程上使用 asyncio,为啥?它是不是提供性能提升?

Posted

技术标签:

【中文标题】我啥时候应该在常规线程上使用 asyncio,为啥?它是不是提供性能提升?【英文标题】:When should I be using asyncio over regular threads, and why? Does it provide performance increases?我什么时候应该在常规线程上使用 asyncio,为什么?它是否提供性能提升? 【发布时间】:2015-07-21 14:20:59 【问题描述】:

我对 Python 中的多线程有相当基本的了解,并且对 asyncio 有更基本的了解。

我目前正在编写一个基于 Curses 的小型程序(最终将使用完整的 GUI,但这是另一回事),它在主线程中处理 UI 和用户 IO,然后有两个其他守护线程(每个使用他们自己的队列/worker-method-that-gets-things-from-a-queue):

watcher 线程监视发生的基于时间和条件(例如,发布到留言板、收到的消息等)事件,然后将所需任务放入... 另一个 (worker) 守护线程的队列然后完成它们。

所有三个线程都在持续并发运行,这引出了一些问题:

worker 线程的队列(或更一般地,任何线程的队列)为空时,是否应该停止它直到再次有事情要做,还是可以继续运行?并发线程除了查看队列之外什么都不做时会占用大量处理能力吗? 是否应该合并两个线程的队列?由于watcher 线程持续运行单个方法,我猜worker 线程将能够从watcher 线程放入的单个队列中提取任务。 我认为这无关紧要,因为我不是多处理,但此设置是否受到 Python 的 GIL(我相信 3.4 中仍然存在)的影响? watcher 线程应该像这样连续运行吗?据我了解,如果我错了,请纠正我,asyncio 应该用于基于事件的多线程,这似乎与我正在尝试做的事情有关。 主线程基本上总是在等待用户按键来访问菜单的不同部分。这似乎适合asyncio 的情况,但我也不确定。

谢谢!

【问题讨论】:

你是如何实现队列的?您使用Queue 模块和/或threading 的同步原语,还是定期检查是否有数据需要处理? @Phillip 是的,我正在使用threading 锁——不知道Queue 有同步原语。这是更好的方法吗? @velocirabbit Queue 模块提供线程安全队列。你绝对应该在你自己创建的任何东西上使用它。 @dano 哦,我肯定在使用Queue——它有内置同步功能吗?那是不是就不用Queue和锁了? @velocirabbit 这完全取决于你如何使用Queue,但如果你只是用它来将消息从生产者传递给消费者,你应该能够使用它没有任何外部锁定。您只需要确保在消费者方面坚持使用queue.get(),而不是在执行get 之前检查Queue 是否为空的某种循环。 【参考方案1】:

当工作线程的队列(或更一般地,任何线程的队列)为空时,应该停止它直到它再次有事情要做,还是可以继续运行?并发线程除了查看队列之外什么都不做时会占用大量处理能力吗?

您应该只使用对queue.get() 的阻塞调用。这将使线程在 I/O 上被阻塞,这意味着 GIL 将被释放,并且不会使用任何处理能力(或至少是非常少的数量)。不要在 while 循环中使用非阻塞获取,因为这将需要更多的 CPU 唤醒。

是否应该合并两个线程的队列?由于观察者线程不断运行单个方法,我猜工作线程将能够从观察者线程放入的单个队列中提取任务。

如果观察者所做的只是将事物从队列中拉出并立即将其放入另一个队列中,在那里它被单个工作人员消耗,这听起来像是不必要的开销 - 你也可以直接在工作人员中消耗它.不过,我不太清楚是否是这种情况 - 观察者是从队列中消费,还是只是将项目放入一个队列中?如果它正在从队列中消费,谁在往里面放东西?

我认为这无关紧要,因为我不是多处理,但是此设置是否会受到 Python 的 GIL(我相信 3.4 中仍然存在)的影响?

是的,这受 GIL 影响。一次只有一个线程可以运行 Python 字节码,因此不会获得真正的并行性,除非线程正在运行 I/O(这会释放 GIL)。如果您的工作线程正在执行 CPU 密集型活动,您应该认真考虑通过 multiprocessing 在单独的进程中运行它,如果可能的话。

观察者线程应该像那样连续运行吗?据我了解,如果我错了,请纠正我,asyncio 应该用于基于事件的多线程,这似乎与我正在尝试做的事情有关。

很难说,因为我不知道“连续运行”到底是什么意思。它在持续做什么?如果它大部分时间都在睡觉或阻塞queue,那很好——这两件事都会释放 GIL。如果它一直在做实际工作,那将需要 GIL,因此会降低应用程序中其他线程的性能(假设它们试图同时工作)。 asyncio 专为受 I/O 限制的程序设计,因此可以使用异步 I/O 在单个线程中运行。听起来您的程序可能非常适合这取决于您的 worker 正在做什么。

主线程基本上总是在等待用户按键来访问菜单的不同部分。 这似乎是 asyncio 最适合的情况,但我也不确定。

任何您主要等待 I/O 的程序都可能适合 asyncio - 但前提是您可以找到一个库,使 curses(或您最终选择的任何其他 GUI 库)可以很好地使用它.大多数 GUI 框架都有自己的事件循环,这将与asyncio 冲突。您需要使用可以使 GUI 的事件循环与asyncio 的事件循环很好地配合的库。您还需要确保可以找到您的应用程序使用的任何其他基于同步 I/O 的库(例如数据库驱动程序)的 asyncio 兼容版本。

也就是说,通过从基于线程的程序切换到基于 asyncio 的程序,您不太可能看到任何形式的性能改进。它的表现可能大致相同。由于您只处理 3 个线程,因此它们之间的上下文切换开销不是很大,因此从单线程异步 I/O 方法切换不会产生很大的不同。 asyncio 将帮助您避免线程同步的复杂性(如果这是您的应用程序的问题 - 目前尚不清楚),并且至少在理论上,如果您的应用程序可能需要 大量 线程,它会更好地扩展,但似乎并非如此。我认为对您而言,这基本上取决于您喜欢使用哪种样式进行编码(假设您可以找到所需的所有 asyncio 兼容库)。

【讨论】:

感谢超级详细的回复!它已经很有帮助,但我会尝试澄清一些关于我正在尝试做的事情的细节:watcher 线程正在运行一个无限的 while 循环,检查是否发生了某些事件。当其中一个事件发生时,它会将特定方法放入worker 的队列中,然后worker 会执行(watcher 有一个队列,其中只有带有循环的函数,但我只是意识到这是不必要的,因为它不会从任何一个队列中消耗任何其他东西)。现在,worker 只是持续监控事件。 所以,线程摘要:main线程处理curses UI和用户I/O,watcher线程监控各种类型的事件(网络帖子、基于时间的等),以及@987654341 @ 执行 watcher 放入其队列的方法(可以是发布帖子/响应、计算事物等)。如果使用asyncio 不会给我带来太多的性能提升,我可能现在只使用threading(因为我在最后期限),然后用asyncio 重写它以进行练习。 upvoted,也感谢您的超级详细回复,我的情况类似,我需要在 node.js 中收听 redis 发布者将数据发送到我接收它的 python,然后需要创建一个 numpy 数组以对其进行一些数学运算,例如移动平均值等,然后使用单独的发布者将结果发布回 node.js,也有一些 IO 绑定操作,例如检查发布者发送的数据是否连续以及它是否连续检测间隙,轮询外部 API 以获取丢失的数据,它是 IO 和 CPU 绑定任务的混合,我假设 asyncio 只做 IO 好并且需要进程......

以上是关于我啥时候应该在常规线程上使用 asyncio,为啥?它是不是提供性能提升?的主要内容,如果未能解决你的问题,请参考以下文章

我啥时候应该在课堂上使用“this”?

我啥时候应该在 NavLink 上使用 Link?

我啥时候应该在 django 模型字段上使用 null=False

我啥时候应该在“class”上使用“className”,反之亦然?

我啥时候应该在“re_path”上使用“path”?

jQuery UI - 我啥时候应该在小部件上使用 destroy 方法