Appearance
34|并发基础
程序同时处理多个任务时,有两种并发方式:多线程和多进程。线程在同一个进程内共享内存,切换开销小;进程有独立的内存空间,能绕开 Python 的 GIL(全局解释器锁),适合 CPU 密集型任务。
一、GIL
Python 的 CPython 实现有一个全局解释器锁(GIL),它确保同一时刻只有一个线程在执行 Python 字节码。这意味着:
- 多线程不能并行执行 Python 代码
- 多线程适合 I/O 密集型任务(网络请求、文件读写、等待用户输入)
- CPU 密集型任务用多进程才能真正并行
GIL 只影响 CPython。Jython、IronPython 等其他实现没有 GIL,但使用较少。
二、threading
创建线程:
python
import threading
import time
def worker(name):
time.sleep(1)
print(f"{name} 完成")
# 创建并启动线程
t1 = threading.Thread(target=worker, args=("线程1",))
t2 = threading.Thread(target=worker, args=("线程2",))
t1.start()
t2.start()
t1.join() # 等待线程结束
t2.join()target 是线程要执行的函数,args 是传给函数的参数(元组)。start() 启动线程,join() 等待线程结束。
三、线程池
手动管理线程麻烦。concurrent.futures.ThreadPoolExecutor 自动管理线程池:
python
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch(url):
time.sleep(1)
return f"{url} 的数据"
urls = ["url1", "url2", "url3", "url4"]
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(fetch, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
result = future.result()
print(f"{url}: {result}")
except Exception as exc:
print(f"{url} 出错: {exc}")max_workers 控制同时运行的线程数。as_completed() 按完成顺序返回结果,不是按提交顺序。
四、线程安全
多个线程同时读写共享数据时,需要同步机制:
python
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(10000):
with lock:
counter += 1
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 50000没有 lock 时,多个线程同时读取和修改 counter,最终结果可能小于 50000(竞争条件)。with lock: 确保同一时刻只有一个线程执行 counter += 1。
五、多进程
CPU 密集型任务(如大量计算、数据处理)用多进程:
python
from concurrent.futures import ProcessPoolExecutor
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
numbers = [11227253509529, 112582705942171, 115280095190773]
with ProcessPoolExecutor(max_workers=3) as executor:
results = executor.map(is_prime, numbers)
for num, result in zip(numbers, results):
print(f"{num}: {result}")多进程的入口必须放在 if __name__ == "__main__": 下面,否则 Windows 上子进程导入模块时会递归创建进程。
六、选择线程还是进程
| 场景 | 推荐方式 |
|---|---|
| 网络请求、文件读写、数据库查询 | 多线程 |
| 大量计算、数据处理、图像处理 | 多进程 |
| CPU 和 I/O 混合 | 根据瓶颈判断,或用 asyncio |
七、asyncio 简介
asyncio 是 Python 的异步 I/O 框架,用单线程事件循环处理多个并发任务:
python
import asyncio
async def fetch(url):
await asyncio.sleep(1) # 模拟网络请求
return f"{url} 的数据"
async def main():
urls = ["url1", "url2", "url3"]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
for result in results:
print(result)
asyncio.run(main())async def 定义协程,await 等待异步操作完成。asyncio.gather() 同时运行多个协程。
asyncio 适合大量 I/O 并发(如同时处理成千上万个连接),但写法与同步代码差异大。普通脚本中线程池通常足够。
记忆锚点:CPython 有 GIL,多线程不能并行执行 Python 代码;多线程适合 I/O 密集型,多进程适合 CPU 密集型;ThreadPoolExecutor 自动管理线程池;共享数据加 Lock 保护;多进程入口放 if __name__ == "__main__" 下;asyncio 适合大量 I/O 并发,但学习成本高。