python 的并发模型
并发指同时处理多件事
并行指同时做多件事
二者不同, 但有联系
一个关于结构, 一个关于执行
并发用于制定方案, 用来解决可能但未必并行的问题
- Rob Pike
Go 语言创造者之一
python 解释器仅使用一个线程运行用户的程序和内存垃圾回收程序, 使用 threading 或 current.future 库可以启动额外的 python 线程, 但是 python 有一个 GIL(Global Interpreter Lock ) 全局解释器锁, 任意时间点上只能有一个 python 线程可以持有 GIL , 这个锁控制对象引用计数和解释器的内部状态, 任意时间点上只有一个线程才能执行 python 代码, 哪怕你有多个 CPU 或者 CPU 有多个核.
为防止一个 Python 线程无限期持有 GIL, pyhon 解释器每 5 毫秒暂停当前线程, 释放 GIL , 由操作系统调度程序来挑选一个等待的线程
Python 标准库中发起系统调用的函数都可以释放 GIL, 例如执行磁盘 IO, 网络 IO, 以及 time.sleep(), 你也可以自己以 C 语言来实现 python 扩展来释放 GIL
所以 IO 密集型的应用程序可以多用 python 线程或者协程, 而计算密集型的应用程序最好使用多进程. 而协程是指可以暂时执行挂起自身并在以后再恢复执行的函数.
什么是并发与并行?
并发与并行是高性能编程中的两个常用术语。尽管它们常常被混淆,但有细微差别。
-
并发:程序通过切换任务来执行多个任务,而这些任务可能并不同时运行。例如,一个程序可以一边下载文件一边处理其他数据,但这些任务是交替执行的。
-
并行:多个任务同时执行,通常在多核处理器上实现。例如,程序可以利用多核处理器同时执行多个任务,每个任务运行在不同的核心上。
Python 对并发的支持
- threading 模块:使用线程实现并发,适用于 I/O 密集型任务。
- asyncio 模块:提供协程支持,实现异步编程,适合大量 I/O 操作。
- multiprocessing 模块:用于多进程,适用于 CPU 密集型任务。
示例 1:使用 threading 模块
threading 模块允许我们创建多个线程来执行任务,线程适用于 I/O 密集型操作,如文件读写或网络请求。
代码示例:
import threading
import time
def task(name):
print(f"线程 {name} 开始")
time.sleep(2) # 模拟 I/O 操作
print(f"线程 {name} 结束")
# 创建线程
threads = []
for i in range(5):
t = threading.Thread(target=task, args=(i,))
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("所有线程执行完毕")
解释:
• 我们创建了 5 个线程,并让它们并发执行任务 task。
• time.sleep(2) 模拟一个耗时的 I/O 操作。
运行结果:
线程 0 开始
线程 1 开始
线程 2 开始
线程 3 开始
线程 4 开始
线程 0 结束
线程 1 结束
线程 2 结束
线程 3 结束
线程 4 结束
所有线程执行完毕
示例 2:使用 asyncio 模块
asyncio 模块提供了协程支持,允许我们以异步方式执行任务,适用于 I/O 密集型任务。它与 threading 的区别在于,它并不依赖多线程,而是依赖于事件循环。
代码示例:
import asyncio
async def async_task(name):
print(f"任务 {name} 开始")
await asyncio.sleep(2) # 模拟异步 I/O 操作
print(f"任务 {name} 结束")
async def main():
tasks = [async_task(i) for i in range(5)]
await asyncio.gather(*tasks)
# 运行事件循环
await main()
解释:
- 我们定义了一个异步任务 async_task,并使用 asyncio.gather 同时运行多个任务。
- await asyncio.sleep(2) 模拟了一个异步的 I/O 操作。
运行结果:
任务 0 开始
任务 1 开始
任务 2 开始
任务 3 开始
任务 4 开始
任务 0 结束
任务 1 结束
任务 2 结束
任务 3 结束
任务 4 结束
示例 3:使用 multiprocessing 模块
multiprocessing 模块允许我们在多个进程上执行任务,适用于 CPU 密集型操作。与线程不同,每个进程拥有独立的内存空间。
代码示例:
import multiprocessing
import time
def cpu_task(name):
print(f"进程 {name} 开始")
time.sleep(2) # 模拟 CPU 密集型操作
print(f"进程 {name} 结束")
# 创建进程
processes = []
for i in range(5):
p = multiprocessing.Process(target=cpu_task, args=(i,))
processes.append(p)
p.start()
# 等待所有进程完成
for p in processes:
p.join()
print("所有进程执行完毕")
解释:
- 这里我们使用 multiprocessing.Process 来创建 5 个独立的进程,每个进程执行 cpu_task。
- 进程间不会共享内存,适合 CPU 密集型任务。
运行结果:
进程 0 开始
进程 1 开始
进程 2 开始
进程 3 开始
进程 4 开始
进程 0 结束
进程 1 结束
进程 2 结束
进程 3 结束
进程 4 结束
所有进程执行完毕
结论
- threading 适用于 I/O 密集型任务,如文件读写、网络请求等。
- asyncio 适合大量 I/O 任务,并且它使用协程代替线程,减少了线程的开销。
- multiprocessing 更适合 CPU 密集型任务,因为它利用了多个进程,每个进程可以使用不同的 CPU 核心。