单线程程序在处理网络请求、文件IO或数据库查询时,CPU往往在等待结果而空转。多线程的核心价值就是让CPU在等待时去执行其他任务,从而提升整体效率。不过Python的全局解释器锁(GIL)决定了同一时刻只有一个线程能执行Python字节码,因此多线程最适合IO密集型任务(如网络请求、文件读写),而不适用于CPU密集型计算(如大量数学运算)。
了解GIL后,我们先从最基础的threading模块入手。以下示例演示如何创建两个线程并让它们并发执行:
- import threading
- import time
- def task(name, delay):
- print(f"{name} 开始")
- time.sleep(delay)
- print(f"{name} 结束")
- t1 = threading.Thread(target=task, args=("任务A", 2))
- t2 = threading.Thread(target=task, args=("任务B", 1))
- t1.start()
- t2.start()
- t1.join()
- t2.join()
- print("全部完成")
复制代码
输出顺序显示两个任务几乎同时启动,总耗时约2秒(取最长的延迟),而非顺序执行的3秒。
实际开发中最常见的多线程场景是并发请求多个API接口。使用threading时需要手动管理线程列表,并通过Lock保护共享数据:
- import threading
- import requests
- urls = [
- "https://api.example.com/1",
- "https://api.example.com/2",
- "https://api.example.com/3",
- ]
- results = []
- lock = threading.Lock()
- def fetch(url):
- resp = requests.get(url, timeout=5)
- with lock:
- results.append(resp.status_code)
- threads = []
- for url in urls:
- t = threading.Thread(target=fetch, args=(url,))
- t.start()
- threads.append(t)
- for t in threads:
- t.join()
- print(f"完成 {len(results)} 个请求")
复制代码
然而手动管理线程较为繁琐,更推荐使用concurrent.futures.ThreadPoolExecutor。它内置线程池,自动管理线程的创建与回收,并提供了简洁的map方法:
- from concurrent.futures import ThreadPoolExecutor
- import requests
- urls = ["url1", "url2", "url3"]
- def fetch(url):
- return requests.get(url).status_code
- with ThreadPoolExecutor(max_workers=5) as executor:
- results = list(executor.map(fetch, urls))
- 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的存在,避免将其用于计算密集型任务,多线程就能显著提升程序响应速度。 |