Python微信订餐小程序课程视频
https://edu.youkuaiyun.com/course/detail/36074
Python实战量化交易理财系统
https://edu.youkuaiyun.com/course/detail/35475
线程池的使用
本章将介绍对线程池进行配置与调优的一些高级选项, 并分析在使用任务执行框架时需要注意的各种危险, 以及一些使用 Executor的高级示例。
一、任务与执行策略之间的隐形耦合
1.1 隐形耦合关系
我们已经知道,Executor框架可以将任务的提交与任务的执行策略解耦开来。这为制定和修改执行策略都提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确地指定执行策略,包括:
a.依赖性任务。
大多数行为正确的任务都是独立的: 它们不依赖于其他任务的执行时序、 执行结果或其他效果。 当在线程池中执行独立的任务时, 可以随意地改变线程池的大小和配置, 这些修改只会对执行性能产生影响。 然而,如果提交给线程池的任务需要依赖其他的任务, 那么就隐含地给执行策略带来了约束, 此时必须小心地维持这些执行策略以避免产生活跃性问题。
b.使用线程封闭机制的任务。
与线程池相比, 单线程的Executor能够对并发性做出更强的承诺。 它们能确保任务不会并发地执行, 使你能够放宽代码对线程安全的要求。 对象可以封闭在 任务线程中,使得在该线程中执行的任务在访问该对象时不需要同步, 即使这些资源不是线程 安全的也没有问题。 这种情形将在任务与执行策略之间形成隐式的耦合—任务要求其执行所在的Executor是单线程的e。如果将Executor从单线程环境改为线程池环境, 那么将会失去线程安全性。
c.对响应时间敏感的任务。
如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性。
d.使用ThreadLocal的任务
ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本“。然而,只要条件允许,Executor可以自由地重用这些线程。在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛出了一个未检查异常,那么将用一个新的工作者线程来替代抛出异常的线程。只有当线程本地值 的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线 程池的线程中不应该使用 ThreadLocal在任务之间传递值。
只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成 “拥塞 ”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。
1.2 线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如 果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务完成,而第一个任务又无法完 成,因为它在等待第二个任务的完成。
在更大的线程池中, 如果所有正在执行任务的线程都由于等待其他仍处在工作队列中的任务而阻塞,那么会发生同样的问题。这种现象被称为饥饿死锁(Thread Starvation Deadlock)。
1.3 运行时间较长的任务
如果任务阻塞的时间过长, 那么即使不出现死锁, 线程池的响应性也会变得糟糕。执行时 间较长的任务不仅会造成线程池堵塞, 甚至还会增加执行时间较短任务的服务时间。如果线程 池中线程的数量远小于在稳定状态下执行时间较长任务的数量, 那么到最后可能所有的线程都会运行这些执行时间较长的任务, 从而影响整体的响应性。
有一项技术可以缓解执行时间较长任务造成的影响, 即限定任务等待资源的时间, 而不要无限制地等待。在平台类库的大多数可阻塞方法中, 都同时定义了限时版本和无限时版本, 例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列以便随后执行。这样, 无论任务的最终结果是否成功, 这种办法都能确保任务总能继续执行下去, 并将线程释放 出来以执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务, 那么也可能表明线程池的规模过小。
二、设置线程池的大小
要设置线程池的大小也并不困难, 只需要避免 “过大” 和 “过小” 这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争, 这不仅会导致更高的内存使用量, 而且还可能耗尽资源。如果线程池过小, 那么将导致许多空闲的处理器无法执行工作,从而降低吞吐率。
要想正确地设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU? 多大的内存?任务是计算密集型、I/0密集型还是二者皆可?等等。
- 对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个 “额外 ” 的线程也能确保CPU的时钟周期不会被浪费。)
- 对于I/O密集型或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程 池的大小,一种方法是通过另来调节线程池的大小:在某个基准负载下,分别设置不同大小的线程池来运行应用程序,并观察CPU利用率的水平。还可以估算出任务的等待时间与计算时间的比值,通过计算获得合适的线程池大小:
Ncpu = number of CPUs
Ucpu = target CPU utilization, 0 ≤ Ucpu ≤ 1
W / C = ratio of wait time to compute time
要使处理器达到期望的使用率,线程池的最优大小等于
Nthread = Ncpu * Ucpu * (1 + W / C)
可以通过 Runtime获得CPU的数目:
Int N_CPUS = Runtime.getRuntime().availableProcessors();
当然,CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。计算这些资源对线程池的约束条件是更容易的:计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所得结果就是线程池大小的上限。
当任务需要某种通过资源池来管理的资源时,例如数据库连接,那么线程池和资源池的大小将会相互影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的 大小。同样,当线程池中的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连 接池的大小。
三、配置 ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由 Executors 中 的newCachedThreadPool、newFixedThreadPool和newScheduledThreadExecutor等工厂方法返回的。 ThreadPoolExecutor是一个灵活的、稳定的线程池,允许进行各种定制。
如果默认的执行策略不能满足需求,那么可以通过 ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制,并且可以参考Executors的源代码来了解默认配置下的执行策略, 然后再以这些执行

本文深入探讨了线程池的高级选项,包括任务与执行策略的耦合、线程饥饿死锁、运行时间较长任务的影响以及如何设置线程池大小。线程池的大小设置要考虑CPU数量、资源预算和任务特性。此外,文章还介绍了配置ThreadPoolExecutor的技巧,如选择无界队列、有界队列和同步移交策略,并讨论了饱和策略如AbortPolicy、CallerRunsPolicy等。通过自定义线程工厂,可以实现线程的定制化创建。最后,文章提出通过扩展ThreadPoolExecutor来添加统计信息和日志记录功能。
最低0.47元/天 解锁文章
173

被折叠的 条评论
为什么被折叠?



