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️⃣ 传统线程模型(已提供)

graph TB APP[应用程序] --> 申请线程 申请线程 --> OS[操作系统内核] OS --> 调度器 调度器 --> 抢占算法 抢占算法 -- 是 --> 中断当前任务 抢占算法 -- 否 --> 分配时间片 中断当前任务 --> 加载新线程 分配时间片 --> 加载新线程 加载新线程 --> 使用CPU 使用CPU --> 硬件资源 硬件资源 --> 返回结果 返回结果 --> APP

2️⃣ 异步协程模型(asyncio)

graph TB APP[应用程序] --> 创建协程 创建协程 --> 事件循环 事件循环 --> 协程调度 协程调度 --> 执行协程 执行协程 --> 是否挂起{是否 await 操作?} 是否挂起 -- 是 --> 挂起并切换 是否挂起 -- 否 --> 继续执行 挂起并切换 --> 其他协程 执行协程 --> 使用CPU 使用CPU --> 硬件资源 硬件资源 --> 返回结果给事件循环 返回结果给事件循环 --> APP

📌 关键点说明:

  • asyncio 模型中,操作系统只看到一个线程(即主线程),所有协程都在这个线程上通过事件循环进行调度。
  • 协程是协作式的多任务机制,不是抢占式的。只有当协程主动 awaityield 时,才会让出控制权。
  • 这种方式减少了线程切换的开销,但牺牲了真正的并行能力(除非配合 concurrent.futures 使用线程池或进程池)。

📊 可视化总结对比图(传统线程和协程)

graph LR subgraph 操作系统视角 ThreadModel[多个线程] AsyncModel[单个线程] end ThreadModel -->|传统线程| APP1[应用程序] AsyncModel -->|asyncio| APP2[应用程序] APP1 --> 多线程编程 APP2 --> 协程编程

您提到的关于Python由于GIL的存在,多线程实际上是并发而不是并行这一点非常重要。基于这一点,我们调整了对比总结,以更准确地反映Python环境下传统多线程模型(考虑到GIL的影响)与异步协程(asyncio)之间的区别。

📊 可视化总结对比图(带GIL锁的线程和协程)

graph LR subgraph 操作系统视角 ThreadModel[多个线程] ThreadModel --> CPU密集任务受限{CPU密集型任务?} CPU密集任务受限 -- 是 --> GIL限制[仅能并发执行] CPU密集任务受限 -- 否 --> I_O操作[高效并发] AsyncModel[单个线程] end ThreadModel -->|传统多线程| APP1[应用程序] AsyncModel -->|asyncio| APP2[应用程序] APP1 --> 多线程编程 APP2 --> 协程编程

在这个更新的版本中,特别强调了传统线程模型虽然在操作系统层面支持多线程,但由于GIL的存在,在CPython解释器中,对于CPU密集型任务,实际上只能达到并发执行的效果,并不能实现真正的并行。而对于异步协程模型(asyncio),它通过事件循环机制提供了高效的I/O操作处理能力,适合处理大量I/O密集型任务或轻量级任务,而无需担心GIL的限制。

asyncio 基本语法

async标识函数
被async标识的函数不在是一个普通函数,而是变成了一个协程函数,调用它会返回一个协程对象
协程函数: 定义形式为 async def 的函数;
协程对象: 调用 协程函数 所返回的对象。

当您定义了一个使用async def的函数时,这个函数就成为了一个协程函数。每次调用这个协程函数时,它并不会立即执行其内部代码,而是返回一个协程对象。要运行协程对象中的代码,则需要通过await表达式或者在另一个异步环境中(如异步函数内)调用它。

主协程逻辑:

  1. 构建事件循环对象
  2. 构建协程对象列表
  3. 收集任务并等待
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. 我们要把刚才写的主协程逻辑(即刚才的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。

  1. 不要用ensure_future了,改为create_task
  2. 最后还要运行下主协程 , 使用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())
posted @ 2025-05-29 13:42  玉米面手雷王  阅读(27)  评论(0)    收藏  举报