aysncio
python 多线程 和 协程
线程 和 协程对比表格
| 特性 | 传统线程(Thread) | 异步协程(async/await, asyncio) |
|---|---|---|
| 调度者 | 操作系统内核 | 用户程序(事件循环) |
| 切换开销 | 高(上下文切换由 OS 完成) | 低(在同一个线程中手动切换) |
| 并行能力 | 多线程并行执行(依赖 CPU 核心) | 单线程内协作式调度(非并行,但高并发) |
| OS 看到的线程数 | 多个 | 通常只有一个 |
| 阻塞影响 | 一个线程阻塞不影响其他线程 | 一个协程阻塞整个事件循环 |
| 特性 | 传统线程(受GIL影响) | 异步协程(async/await, asyncio) |
|---|---|---|
| 调度者 | 操作系统内核 | 用户程序(事件循环) |
| 切换开销 | 高(上下文切换由 OS 完成),但在CPython中受限于GIL,CPU密集型任务无法实现真正的并行执行 | 低(在同一个线程中手动切换) |
| 并发能力 | 单线程内的并发(特别是在I/O等待时可以利用时间片做其他工作) | 单线程内协作式调度(高效并发处理I/O操作) |
| OS 看到的线程数 | 多个,但对CPU密集型任务仅能并发执行而非并行 | 通常只有一个 |
| 阻塞影响 | I/O阻塞不影响其他线程;但对于CPU密集型任务,一个线程阻塞会限制其他线程的执行效率(因为GIL的存在) | 一个协程阻塞不会直接影响其他协程,除非是CPU密集型任务阻塞了整个事件循环 |
🔄 对应 Mermaid 流程图对比
1️⃣ 传统线程模型(已提供)
2️⃣ 异步协程模型(asyncio)
📌 关键点说明:
- 在
asyncio模型中,操作系统只看到一个线程(即主线程),所有协程都在这个线程上通过事件循环进行调度。 - 协程是协作式的多任务机制,不是抢占式的。只有当协程主动
await或yield时,才会让出控制权。 - 这种方式减少了线程切换的开销,但牺牲了真正的并行能力(除非配合
concurrent.futures使用线程池或进程池)。
📊 可视化总结对比图(传统线程和协程)
您提到的关于Python由于GIL的存在,多线程实际上是并发而不是并行这一点非常重要。基于这一点,我们调整了对比总结,以更准确地反映Python环境下传统多线程模型(考虑到GIL的影响)与异步协程(asyncio)之间的区别。
📊 可视化总结对比图(带GIL锁的线程和协程)
在这个更新的版本中,特别强调了传统线程模型虽然在操作系统层面支持多线程,但由于GIL的存在,在CPython解释器中,对于CPU密集型任务,实际上只能达到并发执行的效果,并不能实现真正的并行。而对于异步协程模型(asyncio),它通过事件循环机制提供了高效的I/O操作处理能力,适合处理大量I/O密集型任务或轻量级任务,而无需担心GIL的限制。
asyncio 基本语法
async标识函数
被async标识的函数不在是一个普通函数,而是变成了一个协程函数,调用它会返回一个协程对象
协程函数: 定义形式为 async def 的函数;
协程对象: 调用 协程函数 所返回的对象。
当您定义了一个使用async def的函数时,这个函数就成为了一个协程函数。每次调用这个协程函数时,它并不会立即执行其内部代码,而是返回一个协程对象。要运行协程对象中的代码,则需要通过await表达式或者在另一个异步环境中(如异步函数内)调用它。
主协程逻辑:
- 构建事件循环对象
- 构建协程对象列表
- 收集任务并等待
async def task(i):## 被async标识的函数不在是一个普通函数,而是变成了一个协程函数,调用它会返回一个协程对象
print(f"task {i} start")
await asyncio.sleep(i) ## 模拟io操作,类似爬虫,网页请求等
print(f"task {i} end")
# 下面的语法是python3.9的语法,最新的python 中已经改成了async.run
## 构建事件循环对象
loop = asyncio.get_event_loop()
## 构建协程(coroutine)对象列表,print(task(1)) 可以看到类型是一个协程对象而不是函数
tasks = [task(1), task(2)]
## 收集任务并等待
loop.run_until_complete(asyncio.wait(tasks)) ## 类似线程中的join
asyncio task对象
async def task(i) -> str:## 被async标识的函数不在是一个函数对象,而是变成了一个协程对象
print(f"task {i} start")
await asyncio.sleep(i) ## 模拟io操作,类似爬虫,网页请求等
print(f"task {i} end")
return f"task{i}"
# 下面的语法是python3.9的语法,最新的python 中已经改成了async.run
## 构建事件循环对象
loop = asyncio.get_event_loop()
## asyncio.ensure_future 会把 coroutine 对象包装成 task 对象, 然后从而可以调用一些task对象的方法,比如task.done(), task.result()
tasks = [
asyncio.ensure_future(task(1)),
asyncio.ensure_future(task(2))
]
## 再任务执行前运行.done() 肯定返回false
## print(tasks[0].done())
## 收集任务并等待
loop.run_until_complete(asyncio.wait(tasks)) ## 类似线程中的join
## 所有任务结束后再运行task[0].done 和 .result
print(tasks[0].done())
print(tasks[0].result())
上面的代码中,我们要等所有的tasks都结束后,才去打印了tasks[0]的结果,如果我们想在tasks[0]完成后,立刻查看结果,需要如下代码
## 构建事件循环对象
loop = asyncio.get_event_loop()
## asyncio.ensure_future 会把 coroutine 对象包装成 task 对象, 然后从而可以调用一些task对象的方法,比如task.done(), task.result()
tasks = [
asyncio.ensure_future(task(1)),
asyncio.ensure_future(task(2))
]
## 定义tasks[0]的回调函数
def task01_callback(obj): ## 传参为task object
print(obj) ## 打印task对象
print(obj.done()) ## 打印task对象的.done
print(obj.result()) ## 打印task对象的.result
print("task1 is finished")
tasks[0].add_done_callback(task01_callback)
## 收集任务并等待
loop.run_until_complete(asyncio.wait(tasks)) ## 类似线程中的join
在新版asyncio中,
- 我们要把刚才写的主协程逻辑(即刚才的1. 2. 3. 三点)放在用async 修饰的main()函数中,main()对象其实就是主协程对象
且不用构建实际循环对象了(即loop = asyncio.get_event_loop()省略,main()中已经默认帮你处理了)
收集等待结果的代码也要修改(从loop.run_until_complete(asyncio.wait(tasks)) 改为了 await asyncio.wait(tasks))
同时,可以进行进一步修改, 把 asyncio.wait(tasks) 改成 asyncio.gather(*tasks), 两者基本一样,但是gather有返回值,是所有task.result()的集合列表,方便你查看所有任务的结果,
使用wait 时, 还需要用for 循环 一个个打印 task in tasks。
- 不要用ensure_future了,改为create_task
- 最后还要运行下主协程 , 使用asyncio.run(main())
import asyncio
async def task(i) -> str:## 被async标识的函数不在是一个函数对象,而是变成了一个协程对象
print(f"task {i} start")
await asyncio.sleep(i) ## 模拟io操作,类似爬虫,网页请求等
print(f"task {i} end")
return f"task{i}"
async def main():## 主协程
tasks = [
asyncio.create_task(task(1)),
asyncio.create_task(task(2))
]
res = await asyncio.gather(*tasks)
print(res)
asyncio.run(main())

浙公网安备 33010602011771号