python 并发函数
前言
工作需要向几十万表写入亿级别数据,想使用 pyhon 的并发执行。才开始使用 ThreadPoolExecutor 发现奇慢无比,大佬说这其实是串行,并发得用 multiprocessing,立帖研究。
本文以 python3.10 为例,学习一下 python 的并发执行。
并发执行
python 并发执行分三个方面:多线程(threading)、多进程(multiprocessing)、多协程(asynico)
适当的工具选择主要取决于要执行的任务(CPU 密集型或 IO 密集型):
- CPU 密集型(CPU-bound):也叫做计算密集型,是指 I/O 在很短时间内就可以完成,CPU 需要大量的计算和处理,特点是 CPU 占用率相当高;例如:压缩/解压缩、加密解密、正则表达式搜索、计算;
- IO 密集型(I/O bound):是指系统运作大部分的状况是 CPU 在等 I/O(硬盘,内存)的读写操作,CPU 占用率较低,例如:文件处理,网络爬虫,读写数据库;
在对比这三种方式之前,让我们先了解两个概念:并行和并发
并行和并发的区别
在 Python 中,”并发”和”并行”是两个相关但不同的概念。
并发 (Concurrency)是指程序的设计方式,允许多个任务在重叠的时间段内执行。虽然在同一时刻只能执行一个任务,但任务之间可以通过切换上下文来实现交替执行。这种交替执行的方式可以提高程序的响应性和效率,尤其是在处理 I/O 密集型任务时。在并发编程中,任务之间通常是独立的,它们可以通过多线程、多进程、协程或异步编程等方式来实现。
并行(Parallelism)是指多个任务同时执行的能力。在并行编程中,多个任务真正地同时执行,通常需要多个物理或逻辑处理单元(例如多核 CPU)。并行执行任务可以显著提高计算密集型任务的性能,但对于I/O密集型任务则没有明显的优势。
简单来说,并发是指多个任务在重叠的时间段内交替执行,通过切换上下文实现任务之间的交替执行,以提高程序的响应性和效率;而并行是指多个任务真正地同时执行,通常需要多个物理或逻辑处理单元,用于同时处理不同任务,以提高计算密集型任务的性能。
import time
import concurrent.futures
# 并发执行任务
def task(name):
print(f'Task {name} started')
time.sleep(2)
print(f'Task {name} completed')
# 并行执行任务
def parallel_task(name):
print(f'Task {name} started')
time.sleep(2)
print(f'Task {name} completed')
# 并发示例
with concurrent.futures.ThreadPoolExecutor() as executor:
tasks = ['A', 'B', 'C']
executor.map(task, tasks)
# 并行示例
with concurrent.futures.ProcessPoolExecutor() as executor:
tasks = ['X', 'Y', 'Z']
executor.map(parallel_task, tasks)
在上面的示例中,’task’ 函数模拟了一个耗时 2 秒的任务,并使用线程池实现了并发执行。’parallel_task’ 函数也是一个耗时 2 秒的任务,但使用了进程池实现了并行执行。我们可以运行这段代码,观察任务启动时 python 进程的数目、执行的顺序和时间,以更好地理解并发和并行的区别。
然后,我们来聊 多进程、多线程、多协程的不同、关系以及怎么选择。
进程、线程、协程的对比
- 多进程:
- 优点:可以实现并行,且只有多进程可以实现并行
- 缺点:占用资源多,可启动数目最少
- 多线程:
- 占用资源少,轻量级
- python 的线程是无法并行的(占用多个 cpu),只能进行并发
- 切换线程也是有开销的。
- 适合 IO 密集型运算、同时运行任务不多(线程可启动数量也是有限制的)
- 多协程:
- 优点:内存开销最小,可启动数量最多
- 缺点:支持的库比较少,代码复杂,例如爬虫不支持,所以想用多协程爬取的话,可以用 aiohttp,不能用 requests
- 适用于:IO 密集型、超多任务运行
进程、线程、协程的关系
- 一个进程中可以启动很多线程
- 一个线程中可以启动很多协程
python 慢的原因
两个原因:
- 是解释型语言,边解释边执行
- GIL,无法利用多核 CPU
GIL 是什么,为什么有 GIL?
全局解释器锁(Global interpreter lock),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。
python 设计初期为了解决线程并发的问题引入了 GIL,但是现在很难去除,本质是一种锁,它的好处在于简化了 python 对共享资源的管理,但是导致 python 无法实现真正的多线程执行。
怎样规避 GIL 带来的限制:
- IO 期间线程会释放 GIL,实现 CPU 和 IO 的并发,因此 GIL 的存在对于 IO 密集型计算是有好的,但是对 CPU 密集型则会拖累速度
- 利用 multiprocessing,可以利用多核 CPU 的优势
怎样选择
- IO 密集型运算优先选择多进程
- 若满足三点:需要超多任务量、有现成协程库支持 、代码复杂度可以接受,则选择协程,否则选择线程
threading — 基于线程的并行
3.10.13 Documentation » Python 标准库 » 并发执行 » threading — 基于线程的并行
multiprocessing — 基于进程的并行
3.10.13 Documentation » Python 标准库 » 并发执行 » multiprocessing — 基于进程的并行