相关概念
并发和并行
- 并发:指
一个时间段
内,在一个CPU(CPU核心)能运行的程序的数量。 - 并行:指在
同一时刻
,在多个CPU上运行多个程序,跟CPU(CPU核心)数量有关。
因为
计算机CPU(CPU核心)在同一时刻只能运行一个程序。
同步和异步
- 同步是指代码调用的时候必须等待执行完成才能执行剩余的逻辑。
- 异步是指代码在调用的时候,不用等待操作完成,直接执行剩余逻辑。
阻塞和非阻塞
- 阻塞是指调用函数的时候当前线程被挂起。
- 非阻塞是指调用函数时当前线程不会被挂起,而是立即返回。
CPU密集型和I/O密集型
CPU密集型(CPU-bound):
CPU密集型又叫做计算密集型,指I/O在很短时间就能完成,CPU需要大量的计算和处理,特点是CPU占用高。
例如:压缩解压缩、加密解密、正则表达式搜索。
IO密集型(I/O-bound):
IO密集型是指系统运行时大部分时间时CPU在等待IO操作(硬盘/内存)的读写操作,特点是CPU占用较低。
例如:文件读写、网络爬虫、数据库读写。
多进程、多线程、多协程的对比
类型 | 优点 | 缺点 | 适用 |
---|---|---|---|
多进程 Process(multiprocessing) |
可以利用CPU多核并行运算 | 占用资源最多 可启动数目比线程少 |
CPU密集型计算 |
多线程 Thread(threading) |
相比进程更轻量占用资源少 | 相比进程,多线程只能并发执行,不能利用多CPU(GIL) 相比协程启动数目有限制,占用内存资源有线程切换开销 |
IO密集型计算、同时运行的任务要求不多 |
多协程 Coroutine(asyncio) |
内存开销最少,启动协程数量最多 | 支持库的限制 代码实现复杂 |
IO密集型计算、同时运行的较多任务 |
GIL全称Global Interpreter Lock
下图为GIL的运行
Python的多线程是伪多线程,同时只能有一个线程运行。
一个进程能够启动N个线程,数量受系统限制。
一个线程能够启动N个协程,数量不受限制。
怎么选择
对于其他语言来说,多线程是能同时利用多CPU(核)的,所以是适用CPU密集型计算的,但是Python由于GIL的限制,只能使用IO密集型计算。
所以对于Python来说:
对于IO密集型来说能用多协程就用多协程,没有库支持才用多线程。
对于CPU密集型就只能用多进程了。
协程(异步IO)
简单示例
1 | import asyncio |
单次请求查看结果
1 | import threading |
或者
1 | import threading |
批量请求查看结果
1 | import threading |
asyncio.wait和asyncio.gather
1 | import threading |
asyncio.gather 和asyncio.wait区别:
在内部wait()使用一个set保存它创建的Task实例。因为set是无序的所以这也就是我们的任务不是顺序执行的原因。wait的返回值是一个元组,包括两个集合,分别表示已完成和未完成的任务。wait第二个参数为一个超时值
达到这个超时时间后,未完成的任务状态变为pending,当程序退出时还有任务没有完成此时就会看到如下的错误提示。
gather的使用
gather的作用和wait类似不同的是。
- gather任务无法取消。
- 返回值是一个结果列表
- 可以按照传入参数的 顺序,顺序输出。
协程和多线程结合
同时多个请求
1 | import asyncio |
结果
1 | {"code":0,"msg":"success","obj":{"name":"小明","sex":"男","token":"psvmc"}} |
单个请求添加回调
1 | import asyncio |
多线程
引用模块
1 | from threading import Thread |
数据通信
1 | import queue |
锁
1 | from threading import Lock |
池化技术
1 | from concurrent.futures import ThreadPoolExecutor |
方法单个参数
1 | from concurrent.futures import ThreadPoolExecutor |
结果
1 | ThreadPoolExecutor-0_0 |
方法多个参数
单个请求
1 | from concurrent.futures import ThreadPoolExecutor |
批量请求
1 | from concurrent.futures import ThreadPoolExecutor |
批量请求 全部返回后输出
1 | from concurrent.futures import ThreadPoolExecutor, wait |
批量请求 先返回先输出
1 | from concurrent.futures import ThreadPoolExecutor, as_completed |
多进程
引用模块
1 | from multiprocessing import Process |
数据通信
1 | import multiprocessing |
锁
1 | from multiprocessing import Lock |
池化技术
1 | from concurrent.futures import ProcessPoolExecutor |
方法单个参数
1 | from concurrent.futures import ProcessPoolExecutor |
结果
1 | SpawnProcess-1 |
方法多个参数
单次请求
1 | from concurrent.futures import ProcessPoolExecutor, wait, as_completed |
批量请求 全部返回后输出
1 | from concurrent.futures import ProcessPoolExecutor, wait |
批量请求 先返回先输出
1 | from concurrent.futures import ProcessPoolExecutor, as_completed |
多进程/多线程/协程对比
异步 IO(asyncio)、多进程(multiprocessing)、多线程(multithreading)
IO 密集型应用CPU等待IO时间远大于CPU 自身运行时间,太浪费;
常见的 IO 密集型业务包括:浏览器交互、磁盘请求、网络爬虫、数据库请求等
Python 世界对于 IO 密集型场景的并发提升有 3 种方法:多进程、多线程、多协程;
理论上讲asyncio是性能最高的,原因如下:
- 进程、线程会有CPU上下文切换
- 进程、线程需要内核态和用户态的交互,性能开销大;而协程对内核透明的,只在用户态运行
- 进程、线程并不可以无限创建,最佳实践一般是 CPU*2;而协程并发能力强,并发上限理论上取决于操作系统IO多路复用(Linux下是 epoll)可注册的文件描述符的极限
那asyncio的实际表现是否如理论上那么强,到底强多少呢?我构建了如下测试场景:
请求10此,并sleep 1s模拟业务查询
- 方法 1;顺序串行执行
- 方法 2:多进程
- 方法 3:多线程
- 方法 4:asyncio
- 方法 5:asyncio+uvloop
最后的asyncio+uvloop
和官方asyncio
最大不同是用 Cython+libuv
重新实现了asyncio
的事件循环(event loop)部分,
官方测试性能是 node.js的 2 倍,持平 golang。
顺序串行执行
1 | import time |
多进程
1 | from concurrent import futures |
多线程
1 | from concurrent import futures |
asyncio
1 | import asyncio |
asyncio+uvloop
注意
Windows上不支持uvloop。
示例
1 | import asyncio |
运行时间对比
方式 | 运行时间 |
---|---|
串行 | 10.0750972s |
多进程 | 1.1638731999999998s |
多线程 | 1.0146456s |
asyncio | 1.0110082s |
asyncio+uvloop | 1.01s |
可以看出: 无论多进程、多线程还是asyncio都能大幅提升IO 密集型场景下的并发,但asyncio+uvloop性能最高!