1. 定义
多进程Process:multiprocessing,利用多核CPU的能力,真正的并行执行任务
多线程Thread:threading,利用CPU和IO可以同时执行的原理,让CPU不再等待IO的完成
多协程Coroutine(异步IO):asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行
优点 | 缺点 | 适用于 | |
---|---|---|---|
多进程Process | 可以利用多核CPU并行计算 | 占用资源最多、可启动数目比线程少 | CPU密集型计算 |
多线程Thread | 相比进程,更轻量级、占用资源少 | 相比进程:多线程只能并发执行,不能利用多CPU(GIL) 相比协程:启动数目有限制,占用内存资源,有线程切换开销 | IO密集型计算、同时运行的任务数目要求不多 |
多协程Coroutine(异步IO) | 内存开销最少、启动协程数量最多 | 支持的库有限制、代码实现复杂 | IO密集型计算、需要超多任务运行、但有现成库支持的场景 |
2. 相关知识点
2.1 CPU密集型(CPU-Bound)
CPU密集型也叫计算密集型,是指IO在很短时间就可以完成,CPU需要大量的计算和处理,特点是CPU占用率相当高。例如:压缩解压缩、加密解密、正则表达式搜索、机器训练
2.2 IO密集型(IO Bound)
IO密集型指的是系统运作大部分的状态是CPU在等IO(硬盘内存)的读写操作,CPU占用率仍然较低。例如:文件处理程序、网络爬虫程序、读写数据库程序、网页后端接口程序
2.3 全局解释器锁GIL(Global Interpreter Lock)
是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。即便在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。
多线程用于IO密集型计算在IO读写期间,线程会释放GIL,实现CPU和IO的并行,因此多线程在IO密集型可大幅提示速度,但在CPU密集型计算,因为线程切换的开销,只会拖慢速度。
使用multiprocessing的多进程机制实现并行计算,利用多核CPU优势实现多个解释器的运行,不受GIL的限制。
2.4 多组件的Pipeline技术架构
复杂的事情一般不会一下子做完,而是分为很多中间步骤一步一步完成,如下图所示。
实际应用中,后级任务比前级任务需要更多的资源和时间进行执行,如果直接执行的话,前级任务很快被执行完毕,大多数时间都在等待后级任务的执行。并发编程的思想就是通过分解执行的任务,根据任务的复杂程度和所需要的时间和资源,分配任务的并发数量。
2.5 生产者消费者架构
多组件的Pipeline架构中第一级和第二级组成的架构就叫生产者消费者架构,这也是并发编程应用最多的场景。如下图所示,爬虫程序一般要先从网页中爬取URL,通过URL进行网页信息的解析和存储。前级任务相对简单,可以作为生产者不断生产URL,后级任务需要更多的资源和时间进行内容的解析和入库,需要分配更多的并发数量。
2.6 线程安全
线程安全是指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
2.7 线程的生命周期
3 数据通信
多线程通信:queue.Queue 可用于多线程之间的、线程安全的数据通信
# 1 导入类库
import queue
# 2 创建Queue
thread_queue = queue.Queue()
# 3 添加元素
thread_queue.put("test")
# 4 获取元素
item = thread_queue.get() # 阻塞式的,如果没有数据会一直卡在这里等待获取到数据为止
# 5 查询状态
thread_queue.qsize() # 5.1 查看元素的数量
thread_queue.empty() # 5.2 查看是否为空
thread_queue.full() # 5.3 查看是否已满
4 安全锁
# 1 导入类库
import threading
# 2 实例化一把安全锁
lock = threading.Lock()
# 3 用法1: try-finally 模式
lock.acquire()
try:
# do something
pass
finally:
lock.release()
# 4 用法2: with 模式
with lock:
# do somethin
pass
5 池化技术
新建线程系统需要分配资源、终止线程系统需要回收资源,如果可以重用线程,就可以减去新建和终止线程的开销。
提升性能:因为减去大量新建、终止线程的开销,重用了线程资源
使用场景:适合处理突发大量请求或需要大量线程完成任务、但实际任务处理时间较短
防御功能:能够有效避免系统因为创建线程过多,而导致系统负荷过大相应变慢等问题
代码优势:使用线程池的语法比自己新建线程执行线程更加简洁
# 引入依赖
from concurrent.futures import ThreadPoolExecutor, as_completed
# 用法1 map函数,简单,注意map的结果和入参顺序对应
with ThreadPoolExecutor()as pool:
results = pool.map(method, argus) # method 函数名, argus 参数列表
for result in results:
print(result)
# 用法2 future模式,更强大,结果和入参顺序对应
with ThreadPoolExecutor() as pool:
futures = [pool.submit(method, argus) for argu in argus]
for future in futures:
print(future.result())
# as_completed是按任务完成先后顺序返回
for future in as_completed(futures):
print(future.result())
6 多进程 多线程对比
7 多协程的创建
7.1 旧版本创建协程的方法
import asyncio
async def task1():
while True:
print("Task 1 is running")
await asyncio.sleep(1)
async def task2():
while True:
print("Task 2 is running")
await asyncio.sleep(1)
def main_task():
# 创建一个事件循环
loop = asyncio.get_event_loop()
loop.create_task(task1()) # 在此事件循环中添加异步任务1
loop.create_task(task2()) # 在此事件循环中添加异步任务2
loop.run_forever() # 启动事件循环
7.2 新版本创建协程的方法
import asyncio
async def task1():
while True:
print("Task 1 is running")
await asyncio.sleep(1)
async def task2():
while True:
print("Task 2 is running")
await asyncio.sleep(1)
async def main_task():
task1_coro = asyncio.create_task(task1())
task2_coro = asyncio.create_task(task2())
# main() 函数使用 asyncio.gather() 并发运行 task1(), task2() 这两个任务。
await asyncio.gather(task1_coro, task2_coro)
# 使用 asyncio.run() 启动 main() 函数,这是在 Python 3.7 之后推荐的启动异步函数的方式
asyncio.run(main_task())