并行-两种线程池,以及为什么你需要它们

目录

两种线程池,以及为什么你需要它们

当你使用Python进行大规模数据处理时,线程是实现并行化的好方法。尤其是在进行数值处理时,全局解释器锁(GIL)通常不会成为问题。如果你使用线程,线程池是确保你不会使用过多资源的好方法。

但你的线程池应该有多少线程?你需要一个线程池,还是多个?

在本文中,我们将看到对于数据处理批处理作业:

  1. 有两种线程池,分别适用于不同的用例。
  2. 每种线程池需要不同的配置。
  3. 你可能需要两者。

场景设定:性能架构是情境相关的

没有抽象的“性能”概念。如果我们想选择一个能够快速运行的软件架构,我们需要明确“快速”到底意味着什么,以及我们讨论的是哪种类型的软件。

首先,我们关注的是批处理作业:加载一些数据、处理数据、输出结果然后退出的程序。 虽然底层原则适用于其他类型的应用程序,但根据不同的需求,你可能会得出不同的架构。

其次,性能目标是让整个程序尽快完成。 及时处理中间结果不是优先事项。换句话说,目标是吞吐量,内部任务的延迟只有在影响吞吐量时才重要。这与Web应用程序不同,例如,Web应用程序中单个请求的尾部延迟通常非常重要。

第三,我们假设你的应用程序是机器上唯一运行的应用程序。 也就是说,计算机或虚拟机的所有资源都专用于你的应用程序。在云计算、虚拟机和容器的世界中,这是一个合理的假设。


用于CPU密集型任务的线程池

让我们暂时假设你的程序完全受限于CPU:它从不等待外部资源。鉴于你的目标是让整个程序尽快完成,你希望充分利用所有CPU资源。因此,首先你要确保你的代码可以并行运行。然后,你必须确定要运行多少个线程。

如果你的计算机有8个CPU核心,你希望在任何时候至少有8个线程在运行,以便充分利用这8个核心。当你运行超过8个纯CPU密集型任务的线程时会发生什么?

  1. 只有8个线程可以同时运行。操作系统因此会在线程之间切换,将多余的线程置于睡眠状态。
  2. 线程之间的上下文切换实际上可能会减慢速度,例如由于CPU内存缓存的使用效率降低。

简而言之,如果我们有N个CPU核心,并且只运行CPU密集型任务,我们希望在任何时候正好有N个线程在运行。 线程池是确保这一点的好方法。


用于网络密集型任务的线程池

接下来,让我们考虑另一个极端,假设我们的程序完全受限于网络,等待网络服务器的响应。例如,你可能正在查询数据库,或将指标发送到远程跟踪服务器。如果你处理的是网络密集型任务,CPU核心的数量是无关紧要的,因为大部分时间都在等待,而不是处理。但你仍然需要某种方式来处理多个并发操作。

一种实现方式是使用像asyncio这样的异步事件循环和相应的库。如果你能坚持使用异步库,你根本不需要线程。

但有时你不想使用异步,或者你正在处理阻塞的客户端库。例如,考虑requests.get("https://example.com"):该函数在从服务器获得响应之前不会返回。在此期间,调用此函数的线程无法执行其他操作。

如果没有异步事件循环,如果你想要并发,你需要多个线程。 你需要多少个线程?

作为初步估计,线程数至少应等于你希望同时进行的阻塞操作的数量。 你可以用5个线程向远程服务器发出5个并发网络请求,或用50个线程发出50个并发请求,或用500个线程发出500个并发请求。如果你有更多的线程闲置,那也没关系,至少在一定范围内。

当然,一旦你达到足够的并发性,你将会开始遇到资源限制:数据库连接数、内存、操作系统对文件描述符的限制,或远程服务器的客户限制。因此,设置并发连接数的上限是有用的,线程池是实现这一点的好方法。但池的大小可以比典型的并发水平大得多,而不会产生不良影响。

读写磁盘是另一种任务。通常它很快,但有时如果你饱和了磁盘带宽,它可能会变慢,因此有时使用线程可能会有帮助。


比较两种线程池

以下是两种任务及其对应的线程池大小和目标的总结:

任务类型线程池目标线程池大小
CPU密集型充分利用所有核心CPU核心数
网络密集型防止达到资源限制高于所需并发性,但足够低以避免其他资源限制

处理CPU密集型和网络密集型任务的混合

许多数据处理程序会包含这两种任务的混合:CPU和I/O。如果你对所有I/O使用异步事件循环,你的线程池可以仅用于CPU任务。

但如果你同时进行CPU密集型和阻塞网络操作,使用单个线程池会使你的程序运行得更慢:

  • 如果你根据CPU核心数设置线程池大小,其中一些线程最终会阻塞在网络操作上。结果,你将无法充分利用CPU。此外,你的网络任务的并发性也会受到限制。这意味着你的代码会因为两个不同的原因而运行得更慢!
  • 如果你根据I/O操作的并发性设置线程池大小,你最终会遇到试图运行比核心数更多的CPU密集型任务的情况。有时甚至远远超过你拥有的核心数。如上所述,这可能会导致计算速度变慢,并且可能会显著增加某些应用程序的内存使用量。

与其尝试使用一个无论如何设置都不正确的线程池,不如建议你(至少)使用两个线程池:

  1. 一个用于CPU密集型任务的线程池,大小为核心数。
  2. 一个用于网络密集型任务的线程池,大小为这些任务所需的并发水平。

其他注意事项

首先,一些库会有自己的内部线程池,主要用于计算。如果你管理应用程序级别的线程池,你可能希望禁用这些库的内部线程池。

其次,如果你使用进程池,类似的考虑也适用:网络和CPU有不同的约束,因此有不同的需求。

第三,本文中的建议是特定于情境的(数据处理批处理作业),但也有些通用性。你的应用程序可能对不同类型的网络任务有不同的并发需求,例如具有有限大小的数据库连接池。因此,请将此建议作为起点,而不是最终答案。


总结

在数据处理批处理作业中,线程池的大小和配置取决于任务的性质。CPU密集型任务和网络密集型任务需要不同的线程池配置,混合任务可能需要多个线程池来确保最佳性能。通过合理配置线程池,你可以最大限度地提高程序的吞吐量,避免资源浪费和性能瓶颈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李星星BruceL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值