第53天:Python 线程池

大家都知道当任务过多,任务量过大时如果想提高效率的一个最简单的方法就是用多线程去处理,比如爬取上万个网页中的特定数据,以及将爬取数据和清洗数据的工作交给不同的线程去处理,也就是生产者消费者模式,都是典型的多线程使用场景。

那是不是意味着线程数量越多,程序的执行效率就越快呢。

显然不是。线程也是一个对象,是需要占用资源的,线程数量过多的话肯定会消耗过多的资源,同时线程间的上下文切换也是一笔不小的开销,所以有时候开辟过多的线程不但不会提高程序的执行效率,反而会适得其反使程序变慢,得不偿失。

所以,如何确定多线程的数量是多线程编程中一个非常重要的问题。好在经过多年的摸索业界基本已形成一套默认的标准。

对于 CPU 密集型的计算场景,理论上将线程的数量设置为 CPU 核数就是最合适的,这样可以将每个 CPU 核心的性能压榨到极致,不过在工程上,线程的数量一般会设置为 CPU 核数 + 1,这样在某个线程因为未知原因阻塞时多余的那个线程完全可以顶上。

而对于 I/O 密集型的应用,就需要考虑 CPU 计算的耗时和 I/O 的耗时比了。如果 I/O 耗时和 CPU 耗时 为 1:1,那么两个线程是最合适的,因为当 A 线程做 I/O 操作时,B 线程执行 CPU 计算任务,当 B 线程做 I/O 操作时,A 线程执行 CPU 计算任务,CPU 和 I/O 的利用率都得到了百分百,完美。所以可以认为最佳线程数 = CPU 核数 * [1 +(I/O 耗时 / CPU 耗时]。

线程池

平时我们自己写多线程程序时基本都是直接调用 Thread(target=method) 即可,实际上创建线程远没有这么简单,需要分配内存,同时线程还需要调用操作系统内核的 API,然后操作系统还需要为线程分配一系列的资源,过程很是复杂,所以要尽量避免频繁的创建和销毁线程。

回想一下自己平时写多线程代码的模式,是不是当任务来临时直接创建线程,执行任务,当任务执行结束之后,线程也就随之消亡了。然后又开始循环往复。有多少个任务就创建了多少个线程。这种模式的话很浪费硬件资源。

那如何避免这种问题呢,线程池就派上用场了。

其实线程池就是生产者消费者模式的最佳实践,当线程池初始化时,会自动创建指定数量的线程,有任务到达时直接从线程池中取一个空闲线程来用即可,当任务执行结束时线程不会消亡而是直接进入空闲状态,继续等待下一个任务。而随着任务的增加线程池中的可用线程必将逐渐减少,当减少至零时,任务就需要等待了。

在 python 中使用线程池有两种方式,一种是基于第三方库 threadpool,另一种是基于 python3 新引入的库 concurrent.futures.ThreadPoolExecutor。这里我们都做一下介绍。

threadpool 方式

使用 threadpool 前需要先安装一下,看了这么久我们的文章,相信你很快就会搞定的。在命令行执行如下命令即可。

pip install threadpool

以下是一个简易的线程池使用模版,我们创建了一个函数 sayhello,然后创建了一个大小为 2 的线程池,也就是线程池总共有两个活跃线程。

最后通过 pool.putRequest() 将任务丢到线程池执, pool.wait() 等待所有线程结束。同时我们还可以定义回调函数,拿到任务的返回结果。

由结果我们可以看出,线程池中的确只有两个线程,分别为 Thread-1Thread-2

import timeimport threadpoolimport threading
def sayhello(name): print("%s say Hello to %s" % (threading.current_thread().getName(), name)); time.sleep(1) return name
def callback(request, result): # 回调函数,用于取回结果 print("callback result = %s" % result)
name_list =['admin','root','scott','tiger']start_time = time.time()pool = threadpool.ThreadPool(2) # 创建线程池requests = threadpool.makeRequests(sayhello, name_list, callback) # 创建任务[pool.putRequest(req) for req in requests] # 加入任务pool.wait() print('%s cost %d second' % (threading.current_thread().getName(), time.time()-start_time))
## 运行结果如下Thread-1 say Hello to adminThread-2 say Hello to rootThread-1 say Hello to scottThread-2 say Hello to tigercallback result = admincallback result = rootcallback result = tigercallback result = scottMainThread cost 2 second

ThreadPoolExecutor 方式

ThreadPoolExecutor 是 python3 新引入的库,具体使用方法与 threadpool 大同小异,同样是创建容量为 2 的线程池,提交四个任务。只不过这里分别是通过 submitas_completed 来提交和获取任务返回结果的。

同样由输出结果我们可以看出,两种线程池的实现方式中关于线程的命名方式是不一致的。

import timeimport threadingfrom concurrent.futures import ThreadPoolExecutor, as_completed
def sayhello(name): print("%s say Hello to %s" % (threading.current_thread().getName(), name)); time.sleep(1) return name
name_list =['admin','root','scott','tiger']start_time = time.time()with ThreadPoolExecutor(2) as executor: # 创建 ThreadPoolExecutor future_list = [executor.submit(sayhello, name) for name in name_list] # 提交任务
for future in as_completed(future_list): result = future.result() # 获取任务结果 print("%s get result : %s" % (threading.current_thread().getName(), result))
print('%s cost %d second' % (threading.current_thread().getName(), time.time()-start_time))
## 运行结果如下ThreadPoolExecutor-0_0 say Hello to adminThreadPoolExecutor-0_1 say Hello to rootThreadPoolExecutor-0_0 say Hello to scottThreadPoolExecutor-0_1 say Hello to tigerMainThread get result : rootMainThread get result : tigerMainThread get result : scottMainThread get result : adminMainThread cost 2 second

线程池总结

本文介绍了常用的两种线程池的实现方式,在多线程编程中能使用线程池就不要自己去创建线程,并不是说线程池实现的多么好,其实我们自己完全也可以实现一个功能更强大的线程池。但是其内置的线程池一来是受过全方面测试的,在安全性,性能和方便性上基本就是最优的了,同时线程池还替我们做了很多额外的工作,比如任务队列的维护,线程销毁时资源的回收等都不需要开发者去关心,我们只需注重业务逻辑即可,不需要在关心其他额外的工作,这将大大提高我们的的工作效率和使用感受。

当然其自带的线程池也不是十全十美的,至少暂时没有提供动态添加任务的入口出来。而且在设计方面不够灵活,比如我想线程池只维护一个核心数量,也就是上文说的最大数量。但是当任务过多时可以再额外创建出一些新的线程(阈值可以自定义),处理完之后这些多余的线程将自动销毁,目前这个是做不到的。

代码地址

https://github.com/JustDoPython/python-100-day/tree/master/day-053

参考资料

https://chrisarndt.de/projects/threadpool/api/

系列文章

第52天:Python multiprocessing 模块

第51天:Python Queue 入门

第50天:Python Queue 进阶用法

第49天:Python 多线程之 threading 模块

第48天:初识 Python 多线程

第47天:Web 开发 RESTful

    第46天:Flask数据持久化

第45天:Web表单

第44天:Flask 框架集成Bootstrap

第43天:Python filecmp&difflib模块

第42天:Python paramiko 模块

第41天:Python operator 模块

第0-40天:从0学习Python 0-40合集

(0)

相关推荐

  • 面试官:怎样去运用线程池?工作中如何使用?

    面试官:怎样去运用线程池?工作中如何使用? 工作中,我们有时候需要实现一些耗时的任务.比如:将 Word 转换成 PDF 存储的需求. 假设我们不使用线程池.那么每次请求都会开启新的线程,如果请求过多 ...

  • 写给小白看的线程池,还有10道面试题

    如何搞定20k的面试小抄 为什么要用线程池呢? 下面是一段创建线程并运行的代码: for (int i = 0; i < 100; i++) {     new Thread(() ->  ...

  • 线程(一)——线程,线程池,Task概念+代码实践

    目录 线程(一)--线程,线程池,Task概念+代码实践 12.1 通过TPL进入线程池 12.2 不同过TPL进入线程池 12.3 线程池优化 12.1.1 Task异常捕获 12.2.1 Queu ...

  • 一篇文章浅析Python自带的线程池和进程池

    前言 大家好,我是星期八. 我们都知道,不管是Java,还是C++,还是Go,还是Python,都是有线程这个概念的. 但是我们知道,线程是不能随便创建的,就像每招一个员工一样,是有代价的,无限制招人 ...

  • C#多线程编程(二)线程池与TPL

    一.直接使用线程的问题 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的. 无法直接获取线程函数返回值 无法直接捕捉线程函数内发生的异常 使用线程池可以解 ...

  • python爬虫14 | 就这么说吧,如果你不懂多线程和线程池,那就去河边摸鱼!

    你知道吗? 在我的心里 你是多么的重要 就像 恩 请允许我来一段 freestyle 你们准备好了妹油 你看 这个碗 它又大又圆 就像 这条面 它又长又宽 你们 在这里 看文章 觉得 很开心 就像 我 ...

  • 如何合理地估算线程池大小?

    这个问题虽然看起来很小,却并不那么容易回答. 大家如果有更好的方法欢迎赐教,先来一个天真的估算方法: 假设要求一个系统的TPS(Transaction Per Second或者Task Per Sec ...

  • C#线程学习笔记三:线程池中的I/O线程

    本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/20/MultiThreads.html,记录一下学习过程以备后续查用.     一.I/O线 ...

  • 53个Python库,你必须要试试

    Python库大全 大邓将Python库整理为8部分,对每个库稍加自己的理解和评价,对Python感兴趣的同学可以收藏起来 ·网络爬虫·数据库·数据分析·机器学习·可视化·文本分析·GUI窗体软件开发 ...

  • 多线程之旅(ThreadPool 线程池)

    一.什么是ThreadPool 线程池(源码) 1.线程池顾名思义,有我们的系统创建一个容器装载着我们的线程,由CLR控制的所有AppDomain共享.线程池可用于执行任务.发送工作项.处理异步 I/ ...

  • Java主线程等待子线程、线程池

    print public class TestThread extends Thread { public void run() { System.out.println(this.getName() ...

  • 线程池ThreadPoolExecutor源码分析,看这一篇就够了

    前言 多线程是我们日常工作中很少能接触到的技术,但是面试的时候100%会被问到,万一工作中用到了基本不会,本篇咱们就来深入分析线程池的实现类ThreadPoolExecutor. 1.构造方法 构造方 ...

  • 分析源码,学会正确使用 Java 线程池

    在日常的开发工作当中,线程池往往承载着一个应用中最重要的业务逻辑,因此我们有必要更多地去关注线程池的执行情况,包括异常的处理和分析等.本文主要聚焦在如何正确使用线程池上,以及提供一些实用的建议.文中会 ...