在异步编程中,有时我们需要结合多线程或多进程来突破单线程的限制。本文将详细介绍两种场景的实现方式。
一、场景对比:何时用多线程?何时用多进程?
| 特性 | asyncio + 多线程 | asyncio + 多进程 |
|---|---|---|
| 适用场景 | 多个独立的事件循环 | CPU 密集型计算 |
| 典型任务 | ① 隔离不同业务模块的异步任务 ② 在异步代码中调用不支持async的阻塞库(如requests、PIL、同步数据库驱动) ③ 同步文件IO操作 | 数据分析、图像处理、科学计算 |
| GIL 限制 | 受限(同一时刻只有一个线程执行 Python 代码) | 不受限(每个进程有独立的 GIL) |
| 并发模式 | 并发(concurrent):快速切换 | 并行(parallel):真正同时运行 |
| 资源开销 | 低(共享内存) | 高(独立内存空间) |
| 通信方式 | 共享变量(需加锁) | 进程间通信(队列、管道) |
二、asyncio + 多线程:多事件循环架构
多事件循环架构是指在一个Python程序中,同时运行多个独立的事件循环,每个事件循环在自己的线程中运行,管理各自的异步任务。
使用场景
当你需要:
场景1: 多事件循环架构
- 在不同线程中运行独立的事件循环
- 隔离不同业务模块的异步任务
- 避免单个事件循环被某个任务阻塞
场景2: 调用阻塞库
在异步代码中调用不支持 async 的同步库:
- HTTP 请求库:如
requests(推荐优先使用aiohttp等异步库) - 图像处理:PIL/Pillow、OpenCV
- 同步数据库驱动:如某些老旧的 MySQL 驱动
- 文件 IO 操作:Python 的 asyncio 对文件支持有限
- 任何会阻塞事件循环的第三方库
场景3: 避免阻塞主循环
防止某个慢任务阻塞整个事件循环,影响其他并发任务。
注意:场景2 是最常见的实际应用,但能用异步库就优先用异步库(性能更好)。
代码示例
import asyncio
import threading
import time
from concurrent.futures import ThreadPoolExecutor
# 定义一个异步任务
async def worker_task(worker_name, job_id, duration):
print(f"[{worker_name}] 正在处理任务 {job_id},耗时 {duration}s... (线程: {threading.current_thread().name})")
await asyncio.sleep(duration)
return f"任务 {job_id} 完成"
# 线程执行函数:负责启动并维持该线程专属的事件循环
def start_async_loop(loop, worker_name):
asyncio.set_event_loop(loop)
print(f"线程 {threading.current_thread().name} ({worker_name}) 的事件循环已启动")
loop.run_forever()
def main():
# 1. 创建两个独立的事件循环
loop1 = asyncio.new_event_loop()
loop2 = asyncio.new_event_loop()
# 2. 使用线程池管理线程
with ThreadPoolExecutor(max_workers=2, thread_name_prefix="LoopThread") as executor:
# 提交任务到线程池
executor.submit(start_async_loop, loop1, "Worker-1")
executor.submit(start_async_loop, loop2, "Worker-2")
# 等待事件循环启动
time.sleep(0.5)
start_time = time.perf_counter()
# 3. 从主线程向子线程的事件循环提交异步任务
f1 = asyncio.run_coroutine_threadsafe(worker_task("Worker-1", "Job-A", 2), loop1)
f2 = asyncio.run_coroutine_threadsafe(worker_task("Worker-1", "Job-B", 1), loop1)
f3 = asyncio.run_coroutine_threadsafe(worker_task("Worker-2", "Job-C", 3), loop2)
f4 = asyncio.run_coroutine_threadsafe(worker_task("Worker-2", "Job-D", 1.5), loop2)
# 4. 等待任务完成
print(f"结果反馈: {f1.result()}")
print(f"结果反馈: {f2.result()}")
print(f"结果反馈: {f3.result()}")
print(f"结果反馈: {f4.result()}")
end_time = time.perf_counter()
print(f"多线程计算总耗时: {end_time - start_time:.2f} 秒")
# 5. 停止事件循环
print("\n停止所有事件循环...")
loop1.call_soon_threadsafe(loop1.stop)
loop2.call_soon_threadsafe(loop2.stop)
# 等待线程结束(ThreadPoolExecutor 会在 with 块结束时自动等待)
print("等待线程池关闭...")
print("所有线程已关闭")
if __name__ == "__main__":
main()
关键 API 解析
1. asyncio.new_event_loop()
创建一个新的事件循环对象,用于在独立线程中运行。
loop = asyncio.new_event_loop()
2. asyncio.set_event_loop(loop)
将事件循环设置为当前线程的默认事件循环对象。
asyncio.set_event_loop(loop)
3. asyncio.run_coroutine_threadsafe(coro, loop)
从一个非事件循环线程(即普通线程)向一个正在运行的事件循环(Event Loop)提交协程,返回 concurrent.futures.Future 对象。
future = asyncio.run_coroutine_threadsafe(my_coro(), loop)
result = future.result() # 阻塞等待结果
4. loop.call_soon_threadsafe(callback)
从一个线程向另一个线程的事件循环(Event Loop)排队执行一个普通的同步回调函数。
它是线程安全版本的 loop.call_soon()。由于 asyncio 的大部分方法不是线程安全的,如果你在非 loop 线程直接调用 loop.call_soon(),可能会导致内部状态混乱
loop.call_soon_threadsafe(loop.stop) # 停止事件循环
运行效果
线程 LoopThread_0 (Worker-1) 的事件循环已启动
线程 LoopThread_1 (Worker-2) 的事件循环已启动
[Worker-2] 正在处理任务 Job-C,耗时 3s... (线程: LoopThread_1)
[Worker-2] 正在处理任务 Job-D,耗时 1.5s... (线程: LoopThread_1)
[Worker-1] 正在处理任务 Job-A,耗时 2s... (线程: LoopThread_0)
[Worker-1] 正在处理任务 Job-B,耗时 1s... (线程: LoopThread_0)
结果反馈: 任务 Job-A 完成
结果反馈: 任务 Job-B 完成
结果反馈: 任务 Job-C 完成
结果反馈: 任务 Job-D 完成
多线程计算总耗时: 3.01 秒
停止所有事件循环...
等待线程池关闭...
所有线程已关闭
注意事项
- 必须停止事件循环:
loop.run_forever()会永远运行,必须调用loop.stop()才能退出 - 线程安全:从其他线程操作事件循环时,必须使用
_threadsafe系列方法 - 资源管理:使用
with ThreadPoolExecutor确保线程池正确关闭
三、asyncio + 多进程:CPU 密集型任务处理
使用场景
当你需要:
- 处理CPU 密集型任务(大量计算)
- 绕过 Python 的 GIL(全局解释器锁) 限制
- 充分利用多核 CPU 的计算能力
完整代码示例
import asyncio
import concurrent.futures
import time
import multiprocessing
# 1. CPU 密集型任务:在独立进程中运行
def cpu_bound_calc():
print(f"进程 {multiprocessing.current_process().name} 开始计算...")
# 模拟重度计算
time.sleep(2)
return "计算完成"
# 2. 异步 IO 任务:在主进程的事件循环中运行
async def io_bound_task(name):
while True:
print(f" [IO 任务 {name}] 正在监控网络...")
await asyncio.sleep(1)
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 3. 创建进程池(建议核数一致或根据需求设定)
# ProcessPoolExecutor 会创建多个独立的 Python 进程
with concurrent.futures.ProcessPoolExecutor(max_workers=3) as pool:
# 启动一个背景 IO 任务,证明主循环没被阻塞
asyncio.create_task(io_bound_task("Monitor"))
print("--- 准备向进程池投递计算任务 ---")
start_time = time.perf_counter()
# 4. 使用 loop.run_in_executor 将同步函数转为异步调用
# 这会将 cpu_bound_calc 派发到进程池中的某个进程执行
tasks = [
loop.run_in_executor(pool, cpu_bound_calc),
loop.run_in_executor(pool, cpu_bound_calc),
loop.run_in_executor(pool, cpu_bound_calc)
]
# 等待所有进程返回结果
results = await asyncio.gather(*tasks)
end_time = time.perf_counter()
print(f"\n计算结果: {results}")
print(f"多进程计算总耗时: {end_time - start_time:.2f} 秒")
if __name__ == "__main__":
asyncio.run(main())
关键 API 解析
1. asyncio.get_running_loop()
获取当前正在运行的事件循环(在 async 函数中调用)。
loop = asyncio.get_running_loop()
2. loop.run_in_executor(executor, func, *args)
将同步函数提交到执行器(进程池/线程池)执行,返回一个可 await 的 Future。
future = loop.run_in_executor(pool, cpu_bound_calc)
result = await future
3. asyncio.create_task(coro)
创建并立即调度协程任务,不等待其完成,让任务在后台运行。
task = asyncio.create_task(background_task()) # 立即返回,后台运行
4. ProcessPoolExecutor(max_workers=N)
创建进程池,max_workers 指定进程数量。
with ProcessPoolExecutor(max_workers=4) as pool:
# 使用进程池
运行效果
--- 准备向进程池投递计算任务 ---
[IO 任务 Monitor] 正在监控网络...
进程 SpawnProcess-2 开始计算...
进程 SpawnProcess-1 开始计算...
进程 SpawnProcess-3 开始计算...
[IO 任务 Monitor] 正在监控网络...
[IO 任务 Monitor] 正在监控网络...
多进程计算总耗时: 2.75 秒
关键观察:
- IO 任务(网络监控)持续打印,证明事件循环没有被阻塞
- 3 个 CPU 任务在不同进程中真正并行执行
- 总耗时约 2 秒(而不是串行的 6 秒)
注意事项
- 进程池开销:创建进程比创建线程更耗资源,适合长时间计算
- 数据序列化:进程间通信需要序列化数据(使用 pickle),确保参数和返回值可序列化
四、对比总结
技术选型指南
| 需求 | 方案 | 关键 API |
|---|---|---|
| 多个独立的事件循环 | asyncio + 多线程 | new_event_loop(), run_coroutine_threadsafe() |
| CPU 密集型计算 | asyncio + 多进程 | run_in_executor(), ProcessPoolExecutor |
| IO 密集型任务 | 纯 asyncio | gather(), create_task() |
性能对比
假设有 3 个任务,每个耗时 2 秒:
| 方案 | 总耗时 | 说明 |
|---|---|---|
| 串行执行 | 6 秒 | 一个接一个 |
| asyncio(IO 密集) | 2 秒 | 并发执行,共享单线程 |
| asyncio + 多线程 | 2 秒 | 多个事件循环,但受 GIL 限制 |
| asyncio + 多进程 | 2 秒 | 真正并行,突破 GIL |
最佳实践
- 优先使用纯 asyncio:如果任务是 IO 密集型,单线程就足够了
- CPU 密集型用多进程:避免 GIL 限制,充分利用多核优势
- 多线程用于隔离:需要独立事件循环时使用多线程
- 注意资源管理:使用
with语句确保执行器正确关闭 - 线程安全第一:跨线程操作必须使用
_threadsafe方法来保证线程安全
总结
- asyncio + 多线程:多事件循环架构,适合隔离业务模块
- asyncio + 多进程:突破 GIL,适合 CPU 密集型计算
- 核心思想:让异步代码无缝整合同步世界,既保留异步的高效,又解决单线程的瓶颈
1390



被折叠的 条评论
为什么被折叠?



