请选择 进入手机版 | 继续访问电脑版
查看: 1404|回复: 3

Python多线程入门实战:用ThreadPoolExecutor优化IO密集型任务

[复制链接]
发表于 2026-6-13 16:00:02 | 显示全部楼层 |阅读模式
单线程程序在处理网络请求、文件IO或数据库查询时,CPU往往在等待结果而空转。多线程的核心价值就是让CPU在等待时去执行其他任务,从而提升整体效率。不过Python的全局解释器锁(GIL)决定了同一时刻只有一个线程能执行Python字节码,因此多线程最适合IO密集型任务(如网络请求、文件读写),而不适用于CPU密集型计算(如大量数学运算)。

了解GIL后,我们先从最基础的threading模块入手。以下示例演示如何创建两个线程并让它们并发执行:
  1. import threading
  2. import time
  3. def task(name, delay):
  4.     print(f"{name} 开始")
  5.     time.sleep(delay)
  6.     print(f"{name} 结束")
  7. t1 = threading.Thread(target=task, args=("任务A", 2))
  8. t2 = threading.Thread(target=task, args=("任务B", 1))
  9. t1.start()
  10. t2.start()
  11. t1.join()
  12. t2.join()
  13. print("全部完成")
复制代码

输出顺序显示两个任务几乎同时启动,总耗时约2秒(取最长的延迟),而非顺序执行的3秒。

实际开发中最常见的多线程场景是并发请求多个API接口。使用threading时需要手动管理线程列表,并通过Lock保护共享数据:
  1. import threading
  2. import requests
  3. urls = [
  4.     "https://api.example.com/1",
  5.     "https://api.example.com/2",
  6.     "https://api.example.com/3",
  7. ]
  8. results = []
  9. lock = threading.Lock()
  10. def fetch(url):
  11.     resp = requests.get(url, timeout=5)
  12.     with lock:
  13.         results.append(resp.status_code)
  14. threads = []
  15. for url in urls:
  16.     t = threading.Thread(target=fetch, args=(url,))
  17.     t.start()
  18.     threads.append(t)
  19. for t in threads:
  20.     t.join()
  21. print(f"完成 {len(results)} 个请求")
复制代码

然而手动管理线程较为繁琐,更推荐使用concurrent.futures.ThreadPoolExecutor。它内置线程池,自动管理线程的创建与回收,并提供了简洁的map方法:
  1. from concurrent.futures import ThreadPoolExecutor
  2. import requests
  3. urls = ["url1", "url2", "url3"]
  4. def fetch(url):
  5.     return requests.get(url).status_code
  6. with ThreadPoolExecutor(max_workers=5) as executor:
  7.     results = list(executor.map(fetch, urls))
  8. print(results)
复制代码

通过max_workers参数控制并发数,避免对目标服务器造成过大压力。

使用多线程时需注意三个常见陷阱:

1. GIL导致计算密集型任务不加速:如果线程内执行纯Python循环(如for i in range(10000000): pass),启动10个线程也不会比单线程快。此类场景应改用multiprocessing模块。

2. 共享数据竞争:多个线程同时修改同一个变量可能产生不可预期的结果。使用threading.Lock()加锁即可保证数据安全。

3. 线程数过多:每个线程都有内存和上下文切换开销,盲目开上千个线程反而降低性能。一般建议IO密集型任务的线程数设为CPU核数的2~5倍。

一句话总结:Python多线程的核心价值是让IO等待的时间不被浪费。只要牢记GIL的存在,避免将其用于计算密集型任务,多线程就能显著提升程序响应速度。
回复

使用道具 举报

发表于 2026-6-13 16:10:00 | 显示全部楼层

Re: Python多线程入门实战:用ThreadPoolExecutor优化IO密集型任务

这篇教程写得非常清晰实用!从手动管理线程到ThreadPoolExecutor的演进,确实能帮新手少走很多弯路。特别是GIL的提醒和三个常见陷阱,都是实际开发中容易踩的坑。我之前用多线程爬数据时就因为线程数设太大导致目标服务器限流,后来才意识到要根据场景调整max_workers。想请教下,如果任务既有IO等待又有少量计算(比如解析JSON),是否也适合用多线程?还是说应该考虑多进程+多线程组合?
回复 支持 反对

使用道具 举报

发表于 2026-6-17 11:40:00 | 显示全部楼层

Re: Python多线程入门实战:用ThreadPoolExecutor优化IO密集型任务

楼主的教程写得很清晰,从 GIL 原理到实际代码演进,逻辑非常顺畅,很适合新手入门。 我自己实际用下来,ThreadPoolExecutor 配合 map 确实比手动管线程省心很多,特别是对 API 批量调用这种场景。不过有个小提醒:map 会按传入顺序返回结果,但如果某个请求抛异常(比如网络超时),整个迭代会中断,不太好单独处理。如果对错误容忍度高,可以用 as_completed 或 submit + future 来逐个拿结果并捕获异常。 另外提到线程数建议是 CPU 核数 2~5 倍,这个经验值对大多数网络 I/O 挺准的。但如果是文件读写(比如磁盘 I/O),实际瓶颈可能在磁盘本身,线程数开太多反而会引发排队,有时候调小一点反而更快。 总之感谢分享,期待楼主后续讲讲 multiprocessing 或者 async/await 与多线程的对比。
回复 支持 反对

使用道具 举报

发表于 2026-6-17 14:15:00 | 显示全部楼层

Re: Python多线程入门实战:用ThreadPoolExecutor优化IO密集型任务

感谢楼主的详细讲解!内容非常清晰,尤其是从最基础的 `threading` 到 `ThreadPoolExecutor` 的过渡,很符合实际开发中从手动管理到使用高级工具的思路。你提到的三个陷阱也很实用——特别是第一条,很多新手容易忽略 GIL 对计算密集任务的限制。 我自己在实践中也发现,`ThreadPoolExecutor` 的 `map` 方法虽然简洁,但如果需要处理不同任务(比如有些请求失败后要重试),用 `submit` + `as_completed` 会更灵活。另外,对于 `max_workers` 的设置,除了你说的“CPU 核数 2~5 倍”,还可以根据目标 API 的限流策略适当调低,避免被封。 非常感谢分享,期待你的后续文章!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-17 01:33 , Processed in 0.029067 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部