线程池(thread pool)
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
创建线程对象的期间会损耗时间,尤其是在需要开辟大量线程对象的时候会发生性能下降的情况。那么我们能否让程序创建一定数量的线程对象,并且在执行完某一个任务后不会被解释器销毁,下一个任务重复使用之前所创建的线程对象。
像这种需要创建大量线程对象的场景推荐使用线程池。
import time
from concurrent.futures import ThreadPoolExecutor, as_completed, wait
import requests
def get_image(url):
"""
requests.get(url) 方法获取URL的响应
response.content 获取响应的网页内容
"""
response = requests.get(url).content
# 提取文件名
file_name = url.rsplit('/')[-1]
with open(file_name, 'wb') as f:
f.write(response)
time.sleep(1)
print("下载完成...")
return response
# 创建线程池对象(最大线程为2)
pool_1 = ThreadPoolExecutor(max_workers=2)
url_list = [
'http://pic.bizhi360.com/bbpic/98/10798.jpg',
'http://pic.bizhi360.com/bbpic/92/10792.jpg',
'http://pic.bizhi360.com/bbpic/86/10386.jpg'
]
all_task = [pool_1.submit(get_image, img_url) for img_url in url_list]
# 运行线程池后
# wait()让主线程阻塞,等待所有任务完成后接着运行
wait(all_task)
print("主线程结束运行...")
# 关闭线程池
pool_1.shutdown()
# 方式一:
for img_url in url_list:
# submit()用于提交任务,并且返回类型为future类型的值
future = pool_1.submit(get_image, img_url)
# result()用于获取所有任务的返回值,并且在所有任务完成后一起返回
print(future.result())
# 方式二
# 将所有任务的返回值存储在 列表 all_task中
all_task = [pool_1.submit(get_image, img_url) for img_url in url_list]
as_completed(all_task) # 返回一个迭代器
# 获取所有返回值,然后一次性提交
for future in as_completed(all_task):
print(future.result())
# 方式三
# map可以提交任务,当前任务的参数可以使用迭代对象进行传递
for res in pool_1.map(get_image, url_list):
pass
比喻环节(更好理解)
大学生需要去水房接水
当有一个人接水时,就要使用一个水龙头,多一个人时又要使用一个水龙头
那么我们预先创建一定数量的线程(五个水龙头),当六个人来水房用水时,先让五个人使用,另一个人等待,有人用完后离开,水龙头空闲(线程返回线程池等待下一个任务),等待的人上前使用。
线程池的优缺点
优点:
- 重用线程,避免了频繁创建和销毁线程的开销。(重复开关水龙头,1000人挨个使用一个水龙头,会坏掉的)
- 通过限制线程池的大小,可以避免过多的线程竞争资源,从而提高程序的稳定性。(防止等的人太多,都想用然后打起来)
- 线程池可以方便地管理线程的生命周期,简化了线程的同步和通信。(哪个水龙头坏了修哪个,不用直接修主水管)
缺点:
- 如果线程池中的线程数量设置不当,可能会导致资源浪费或性能瓶颈。(1000个水龙头没必要【浪费】,但三个水龙头又不够10个人用【性能瓶颈】)
- 线程池的实现和管理相对多线程来说更复杂一些。(注意线程池的线程执行的是相同的任务,而多线程是一个程序创建多个线程执行不同的任务)