Python-并发编程
文章目录
- 1. 简介
- 2. 全局解释器锁GIL
- 3. 多线程
- 4. 多进程
- 5. 异步IO
1. 简介
- 基础概念:
- 并行:利用多核CPU同时运行;
- 并发:伪并行,同一时刻一个线程运行时,其它线程挂起;
- 同步:提交一任务,等待返回值,再提交一个任务
- 异步:提交一个任务,不等待返回,再提交一个任务
- 阻塞:程序运行时,遇到IO程序挂起;
- 非阻塞:程序顺序执行,不会卡住;
- 任务类型:
| 类型 | IO时间 | CPU占用率 | 例子 |
|---|
| CPU密集型 | 短 | 高 | 压缩解压、加密解密、正则匹配 |
| IO密集型 | 长 | 低 | 文件读写、DB读写、网络请求 |
- 方法选择:
| 方法 | 优缺点 | 场景 |
|---|
多进程 Process 包 multiprocessing | 优点:可以利用多核CPU并行运算 缺点:占用资源最多、可启动数目比线程少 | CPU密集型 |
多线程 Thread 包 threading | VS 进程-优点:占用资源少 VS 进程-缺点:只能并发执行,不能利用多CPU VS 协程-缺点:启动数目有限,占用内存资源,有切换开销 | IO密集型 少量任务 |
多协程 Coroutine 包 asyncio | 优点:内存开销最少、启动协程数量最多 缺点:支持的库有限制(aiohttp vs requests)、代码实现复杂 | IO密集型 大量任务 |
2. 全局解释器锁GIL
- Python速度慢原因:
- 动态解释型语言,边解释边执行;
- 全局解释器锁GIL,无法利用多核CPU实现并发。
- 全局解释器锁GIL:多线程之间的切换会导致引用计数器计数混乱、数据不完整、状态异常等问题。因此,提出GIL(Global Interpreter Lock)使得同一时刻也只有一个线程在执行。
- GIL限制下加速方法:
- 多线程:IO密集型任务,实现CPU和IO并行加速;CPU密集型任务,由于频繁的线程切换会拖慢速度。
- 多进程:利用多核CPU实现并行计算。
3. 多线程
- 创建:
import threading
def my_func(a, b):
pass
# 创建线程对象
t = threading.Thread(target=my_func, args=(100, 200))
t.start() # 启动线程
t.join() # 等待结束
- 生产者消费者架构:
- 例子:信件与邮递员
- 定义:生产者把数据放入缓冲区,消费者从缓冲区取出数据进行处理。
- 多线程数据通信:
import queue
q = queue.Queue() # 创建队列缓冲区
q.put(item) # 生产者把数据放入缓冲区(阻塞的,满时卡住)
item = q.get() # 消费者从缓冲区取出数据(阻塞的,空时卡住)
q.qsize() # 查看缓冲区元素个数
q.empty() # 判断缓冲区是否为空
q.full() # 判断缓冲区是否已满
- 线程安全:多线程切换时,能够正确地处理多个线程之间的共享数据。
import threading
lock = threading.Lock()
# 解决方法1:
lock.acquire()
# do something
lock.release()
# 解决方法2:
with lock:
# do something
- 线程池:
- 线程生命周期:新建线程系统需要分配资源、终止线程系统需要回收资源。线程池可以重用线程,从而减去新建/终止的开销。
- 线程池优点:
- 提升性能:减去了大量新建、终止线程的开销,重用了线程资源;
- 防御功能:能有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题
- 代码优势:使用线程池的语法比自己新建线程执行线程更加简洁
- 适用场景:适合需要大量线程完成任务、但实际任务处理时间较短
- 实现:
from concurrent.futures import ThreadPoolExecutor, as_completed
# 第一种:
with ThreadPoolExecutor() as pool:
results = pool.map(craw, urls) # map的结果和入参是顺序对应的
for result in results:
print(result)
# 第二种:
with ThreadPoolExecutor() as pool:
futures = [ pool.submit(craw, url) for url in urls ]
for future in futures: # 顺序返回
print(future.result())
for future in as_completed(futures): # 谁先执行完,先返回谁
print(future.result())
4. 多进程
- 原理:多进程在多个CPU上并行执行,适用于CPU密集型计算。
- 对比梳理:
5. 异步IO
- 协程:使用超级循环在单线程内实现并发。
- 实现:request 不支持异步,需要使用aiohttp
import asyncio
loop = asyncio.get_event_loop() # 获取事件循环
async def myfunc(url): # 定义协程
await get_url(url) # 对应IO,执行到此处不阻塞
tasks = [loop.create_task(myfunc(url)) for url in urls]# 创建任务列表
loop.run_until_complete(asyncio.wait(tasks)) # 执行等待完成
- 信号量:设置并发量Semaphore,控制并发数量。
sem = asyncio.Semaphore(10)
# 方法1
async with sem:
# work with shared resource
# 方法2
await sem.acquire()
try:
# work with shared resource
finally:
sem.release()