python3从入门到精通(四): python协程

一、理论知识

1.1、主进程、主协程 和 主协程

概念本质核心特征所属层级
主进程操作系统分配资源的最小单位拥有独立的内存空间(如堆、栈、文件句柄),是程序运行的容器;一个程序默认启动一个主进程操作系统层面
主线程CPU 调度执行的基本单位进程内的第一个线程(也称 “初始线程”),进程启动时自动创建;线程共享进程的资源进程内部
主协程用户态的轻量级执行单元协程框架(asyncio)中,由主线程启动的根协程;协程不占用CPU调度,由程序自身调度线程内部(通常主线程)

主协程不是某个具体的函数/方法,而是对主线程中默认执行流的抽象:
它没有固定的名称,也不是可调用的方法;
它的执行内容由代码的入口逻辑决定(if name == ‘main’ 代码块 或 顶层代码);
它是所有子协程的父协程,子协程的调度依赖主协程的"让出 CPU"操作
主进程(资源容器)
└── 主线程(CPU执行入口)
└── 主协程(用户态根协程)
└── 子协程(由主协程调度)

# 核心区别
1. 调度层面
主进程/主线程: 由操作系统内核调度--"抢占式调度"
  * 比如CPU时间片用完、更高优先级进程/线程到来,内核会强制切换,程序员无法完全控制
主协程: 由用户态的协程框架调度--"协作式调度"
  * 只有协程主动让出控制权"await",其他协程才能执行,内核完全感知不到协程的存在

2. 资源层面
主进程: 资源独立。多个进程之间内存、文件句柄等不共享
       主进程退出,其所有资源被回收
主线程: 共享进程资源。同一进程内的所有线程共享进程的堆、全局变量、文件句柄等
       主线程退出,进程会被强制终止(其他线程也会被杀死)
主协程: 共享线程资源。同一线程内的所有协程共享线程的栈、寄存器等
       主协程退出,该线程内的其他协程也会被终止(如asyncio的main()协程结束,事件循环停止)

2. 生命周期
主进程的终止: 只要进程内的主线程执行完毕(或被强制杀死),主进程就会退出,操作系统会回收该进程的所有资源(包括所有线程、协程)
主协程的终止: 主协程就是主线程里的代码执行流,主协程执行完(比如main函数跑完),主线程就会结束,进而导致主进程退出

3. 控制范围不同
主进程控制所有线程/协程的生死: 哪怕子协程还在执行,只要主进程退出,所有协程都会被操作系统强制终止
主协程控制子协程的调度时机: 主协程执行I/O等"让出CPU"的操作时,调度器才会切换到子协程执行
import asyncio
import threading
import os
# 主进程:整个程序运行的容器,PID是os.getpid()
print(f"主进程PID: {os.getpid()}")

def sub_thread():
    print(f"子线程ID: {threading.get_ident()}")

async def sub_coroutine():
    print("子协程执行")
    await asyncio.sleep(1)

async def main():  # 主协程
    print(f"主协程运行的线程ID: {threading.get_ident()}")
    await sub_coroutine()

if __name__ == "__main__":
    # 主线程:进程内的第一个线程,执行入口代码
    # 主线程执行以下代码
    print(f"主线程ID: {threading.get_ident()}")
    
    # 启动子线程(隶属于主进程)
    t = threading.Thread(target=sub_thread)
    t.start()
    t.join()
    
    # 主协程:asyncio的根协程,运行在主线程中
    # 启动主协程(运行在主线程中)
    asyncio.run(main())
    # 主线程执行完毕→主进程退出
    
# 输出逻辑:
主进程先启动,输出PID
主线程执行入口代码,输出自身ID
主线程创建并启动子线程,子线程输出ID
主线程启动 asyncio 事件循环,执行主协程main(),主协程调度子协程
主协程执行完毕→事件循环关闭→主线程继续执行→主线程结束→主进程退出

1.2、进程、线程 和 协程

进程是 独立的资源容器,线程是容器内的CPU执行流,协程是执行流内的轻量级微任务
进程/线程由内核抢占式调度,协程由用户态协作式调度;进程隔离性最强、开销最大,协程隔离性最弱、开销最小

概念本质定位核心特征依赖 / 归属
进程资源分配最小单位拥有独立的内存空间(堆、栈、文件句柄、环境变量等),进程间资源隔离;启动 / 销毁开销大操作系统内核管理,独立存在
线程CPU 调度基本单位隶属于进程,多个线程共享进程的所有资源;线程有独立的栈和寄存器,启动 / 销毁开销中等依赖进程,同一进程内多线程共享资源
协程用户态轻量级执行单元隶属于线程,共享线程的所有资源;无内核调度开销,由程序(协程框架)协作式调度依赖线程,同一线程内多协程串行执行
技术核心场景举例
进程隔离性要求高、CPU 密集型任务浏览器多标签页、数据库服务多实例、大型计算任务
线程IO 密集型 / CPU 密集型、中等并发后端接口线程池、桌面应用多任务(如下载+UI)
协程高并发 IO 密集型任务百万级 HTTP 请求处理、消息队列消费、爬虫
维度进程线程协程
调度者操作系统内核操作系统内核用户态协程框架
调度方式抢占式抢占式协作式
资源开销极低
资源共享进程间隔离进程内共享(需锁)线程内共享(无锁)
并行能力支持支持单线程内不支持
错误隔离
典型并发数百级千级百万级
适用场景CPU 密集、高隔离中等并发、混合任务高并发 IO 密集

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

# 核心区别
1. 调度方式: 内核态抢占式 vs 用户态协作式
进程/线程: 由操作系统内核调度,采用抢占式调度,比如CPU时间片用完、更高优先级任务到来,内核会强制切换,程序员无法完全控制调度时机
例: CPU同时运行QQ和微信进程,内核会交替分配时间片;同一进程内的下载线程和UI线程,内核也会抢占式切换
协程: 由用户态的协程框架(asyncio)调度,采用协作式调度,只有协程主动让出控制权,如await/yield,其他协程才能执行,内核完全感知不到协程的存在
例: Python中一个线程内的A协程执行await asyncio.sleep(1)时,主动让出控制权,B协程才能执行;若A协程无await,会一直占用线程,B协程永远无法执行

2. 并发/并行能力
进程: 支持并行(多CPU核心同时运行多个进程),但并发数有限(通常百级),因资源开销大
线程: 支持并行(多CPU核心同时运行多个线程),并发数中等(通常千级),受内核线程数限制
协程: 单线程内的协程只能串行执行(无并行),但可通过 "多线程 + 多协程"实现并行;协程的并发数极高(通常百万级),适合IO密集型场景
例:处理10万条 HTTP 请求,用线程需创建10万个线程(内存溢出),用协程仅需1个线程 + 10 万个协程(极低开销)

3. 错误隔离: 强 vs 弱 vs 无
进程: 隔离性最强,一个进程崩溃不会影响其他进程(操作系统会回收其资源)
线程: 隔离性弱,一个线程崩溃会导致整个进程崩溃(因为线程共享进程资源)
协程: 无隔离性,一个协程崩溃会导致所属线程崩溃,进而可能导致进程崩溃

4. 资源共享: 隔离 vs 进程内共享 vs 线程内共享
进程: 资源完全隔离,进程间通信需通过IPC(Pipe、Queue等),成本高
例: Chrome的每个标签页是独立进程,一个标签页崩溃不会影响其他标签页
线程: 同一进程内的所有线程共享进程的堆、全局变量、文件句柄等资源,仅栈和寄存器独立;线程间通信可直接读写共享变量(需加锁避免竞态)
协程: 同一线程内的所有协程共享线程的全部资源(栈、寄存器、全局变量等);协程间通信可直接读写变量(无线程安全问题,因为同一线程内串行执行): Python asyncio中,主协程和子协程可直接共享一个列表,无需加锁

5. 资源开销: 高 vs 中 vs 极低
维度	            进程	                          线程	                      协程
内存占用	   大(独立地址空间)(共享进程内存,仅独立栈)	极小(共享线程栈,仅少量上下文)
启动/销毁   慢(毫秒级)(微秒级)	                极快(纳秒级)
切换开销	   大(内核态切换,需保存完整上下文)(内核态切换,上下文较少)	极小(用户态切换,仅保存寄存/栈指针)

# 核心联系
1. 层级关系
进程(资源容器)
└── 线程(CPU执行流)
    └── 协程(用户态微执行流)
线程不能脱离进程存在: 线程是进程的"执行分支",进程启动时自动创建"主线程",所有线程都隶属于某个进程
协程不能脱离线程存在: 协程是线程的"执行片段",所有协程都运行在某个线程内,线程退出则其内部所有协程终止
多进程 ≠ 多线程 ≠ 多协程: 一个进程可包含多个线程,一个线程可包含多个协程

2. 生命周期
进程退出 → 其所有线程强制终止 → 线程内所有协程终止
线程退出 → 仅影响自身协程,不影响同进程内的其他线程(除非是主线程:主线程退出会导致进程强制退出)
协程退出 → 仅影响自身,不影响同线程内的其他协程(除非是主协程:主协程退出会关闭事件循环)

进程: 最重、最安全,适合隔离型任务;
线程: 资源共享、灵活高效,但需小心线程安全问题;
协程: 极致轻量,适用于高并发、I/O 密集型任务。

1.3、什么是协程(Coroutine)?

  1. 协程,又称微线程纤程。是python中一种实现多任务的方式,比线程更小,占用资源更少。自带CPU上下文,可以在合适的时间把一个协程切换到另一个协程
  2. 协程是一种‌用户态的轻量级并发编程模型‌,比线程更轻量级,它在单线程内通过协作式任务切换实现高效并发,避免了线程上下文切换的开销,特别适合处理I/O密集型任务
  3. 协程是用户态的轻量级执行单元,也常被称为微线程用户级线程,它隶属于线程、由程序自身调度,核心特征是协作式调度极低的切换开销,是实现高并发 IO 密集型任务的关键技术
  4. 协程拥有自己的寄存器和栈。协程调度切换的时候,将寄存器上下文和栈都保存到其他地方,在切换回来的时候,恢复到先前保存的寄存器上下文和栈,因此:协程能保留上一次调用状态,每次过程重入时,就相当于进入上一次调用的状态
  5. 协程是运行在用户态的轻量级子程序,是由用户代码而非操作系统内核调度的并发执行单元。它能在执行过程中主动暂停保存完整上下文:变量、执行位置、栈状态等,待条件满足后恢复执行,且恢复后完全保留暂停前的状态,是比线程更轻量的并发编程模型
  6. 协程是单线程+异步I/O的编程模型,有了协程的支持,可基于事件驱动编写高效的多任务程序。
    6.1. 协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。
    6.2. 协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

工作原理:
‌1. 协作式调度‌: 任务遇到I/O阻塞等场景时主动让出CPU,由事件循环调度其他协程执行
‌2. 生成器基础‌: 早期通过yield关键字实现,现代Python使用async/await语法定义协程函数
‌3. 事件驱动‌: 需配合asyncio模块的事件循环来实现异步操作,事件循环负责监听和分发事件,如网络请求、文件IO等,当协程遇到await时,它会将控制权交还给事件循环,同时挂起自身的执行。这样事件循环可以继续处理其他的任务,直到被挂起的循环可以继续执行时,事件循环再将控制权交回给该协程。协程封装为Task对象后由事件循环管理。

# 协程的本质:
1. 协程是"协作式"调度,需主动"await"让出控制权,若协程内有阻塞操作,会阻塞整个事件循环
2. "用户态可控的可暂停子程序",核心价值是通过极低的调度开销和可暂停恢复的能力,解决IO密集型高并发场景下线程开销过高、异步代码复杂的问题,是现代高并发编程的核心模型之一️

# 核心概念: 
1. 低资源消耗‌: 无需创建多线程/进程,内存占用更小。‌‌
2. 高并发性能‌: 单线程可承载数万协程,I/O密集型任务效率显著提升。‌‌
‌3. 无锁编程‌: 协程间数据共享无需加锁,简化并发逻辑。‌‌
4. 用户态调度: 由程序自主控制调度,不涉及操作系统内核,上下文切换成本远低于线程。切换时机明确(遇到I/O操作、显式让出等)
5. 协作式多任务: 协程主动让出CPU控制权,而不是被强制抢占,需要协程间相互配合
6. 单线程并发‌: 在单个线程内通过任务主动让出控制权实现多任务交替执行,规避Python的GIL限制
7. ‌状态保存‌: 暂停时自动保存执行上下文(寄存器、栈帧等),恢复时从断点继续

# 概念的核心要素:
1. 调度主体: 用户态主导(区别于线程/进程)
  协程的调度权完全由用户层代码(Python的asyncio)掌控,而非操作系统内核。
  * 线程/进程的调度是内核级的(抢占式): 内核会强制中断正在执行的线程,切换到其他线程
  * 协程的调度是用户级的(协作式): 协程主动"让出CPU"(await),切换到同线程内的其他协程,无内核切换的额外开销
2. 核心能力: 可暂停/可恢复-"协程的本质特征"
  * 普通函数: 调用后必须从头执行到结束,执行过程中无法暂停,返回后上下文销毁
  * 协程: 执行到指定节点(如等待IO、等待其他协程结果)时,可主动"挂起"并保存所有上下文,返回调用点;待触发条件满足后,能从挂起的位置继续执行,上下文完全保留。
3. 运行载体: 依附线程存在,协程不能脱离线程独立运行,一个线程可承载成百上千个协程-"多协程复用单线程"
  * 协程的执行最终依赖线程的CPU时间片
  * 若承载协程的线程被内核阻塞(如执行CPU密集型任务、调用阻塞式IO),该线程下的所有协程都会暂停
4. 资源特征: 极致轻量
  * 协程的栈空间仅几KB到几十KB(可动态扩展),远小于线程(默认MB级栈空间)
  * 创建、切换、销毁协程的开销仅为线程的千分之一甚至万分之一,因此单线程可轻松支撑百万级协程并发

# 概念的易混淆点
1. 协程≠异步: 协程是执行模型,异步是编程范式("非阻塞执行");协程是实现异步的一种优秀方式(让异步代码写起来像同步代码),但异步也可通过回调、事件循环等实现
2. 协程≠并行: 单线程内的协程是"并发"(交替执行)而非"并行"(同时执行);"多线程+协程"才能实现协程的并行执行
3. 协程不适合CPU密集型任务: 单线程内的协程串行执行,CPU密集型任务会占满线程,导致其他协程无法执行,此类场景仍需多线程/多进程
4. 因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是"多进程+协程",既充分利用多核,又充分发挥协程的高效率,可获得极高的性能

# 子程序:
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
 * 子程序调用是通过栈实现的,一个线程就是执行一个子程序。
 * 子程序调用总是一个入口,一次返回,调用顺序是明确的。
协程的调用和子程序不同,协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行

# 协程的优势:
1. 极高的执行效率: 因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销。和多线程比,线程数量越多,协程的性能优势就越明显
2. 不需要多线程的锁机制: 因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
4. 高并发+高扩展+低成本: 一个cpu支持上万的协程都没有问题,适合用于高并发处理
5. 资源利用率高,减少线程空闲等待时间

# 协程的缺点:
1. 无法利用多核的资源,协程本身是个单线程,它不能同时将单个cpu的多核用上,协程需要和进程配合才能运用到多cpu上(协程是跑在线程上的)
2. 进行阻塞操作时会阻塞掉整个程序

# 协程来回切换执行的意义何在呢?
1. 计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能。
2. IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码)。

# 异常处理:
协程的异常需显式捕获(try/except),未捕获的异常会导致Task崩溃

1.4 、并行/并发+同步/异步 阻塞/非阻塞 + 四大I/O模型

1.4.1、并行和并发

# 并行(Parallelism)
在同一时刻执行多个任务或指令,通常是在多个处理单元(如多个CPU核心或多个GPU 核心)上同时执行。
1. 每个任务都是独立执行的,彼此之间不会受到影响,且执行顺序不受限制。
2. 并行的目标是通过同时处理多个任务来提高整体的计算速度。
# 并发(Concurrency)
在相同时间段内执行多个任务,这些任务可能会交替执行,但并不一定是同时执行的。
1. 多个任务之间可能会共享资源,因此需要考虑资源竞争和同步问题。
2. 并发的目标是更高效地利用计算机资源,提高系统的吞吐量和响应性。

在这里插入图片描述

1.4.2、同步、异步

# 同步Sync 和 异步Async
同步: 任务按顺序执行,上一个任务在没有等到结果返回之前,不会执行后续任务
异步: 任务发起后,不等待结果返回,继续执行后续任务。当任务完成后,通过状态、通知和回调来通知调用者
# 对调用者通知的三种方式: 
1. 状态: 监听任务状态(轮询),调用者需要每隔一定时间检查一次,效率很低
2. 通知: 当任务执行完成后,发出通知告知调用者,无需消耗太多性能
3. 回调: 当任务执行完成后,会调用调用者提供的回调函数

# 异步 和 多线程: 
异步是目的,多线程是实现这个目的的方法
异步: 非阻塞式执行,任务发起后不等待结果返回,主线程继续执行其他任务;当异步任务完成,通过回调/事件通知主线程处理结果
多线程: 并发执行多个线程,线程是CPU调度的最小单位,多个线程共享进程资源,由操作系统调度在CPU上交替执行(多核CPU可并行)

1.4.3、阻塞、非阻塞

# 阻塞Blocking 和 非阻塞Non-blocking
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时调用方的状态
阻塞: 调用方发起请求后,任务执行期间,调用方被挂起(暂停),必须等待任务完成,期间无法执行任何操作
非阻塞: 调用方发起请求后,无需等待任务完成,立即返回(无论结果是否返回),可继续执行其他操作

1.4.4、同步阻塞、同步非阻塞、异步阻塞、异步非阻塞模型

1.4.4.1、同步阻塞(Synchronous Blocking)

调用方发起任务后,既等待结果(同步),又被挂起无法做其他事(阻塞),直到任务完全完成
典型场景: 普通的同步I/O ,如 Python requests.get

def sync_blocking():
    start_time = time.time()
    # 同步阻塞:发起请求后,主线程挂起,直到响应返回
    resp1 = requests.get("https://www.baidu.com")
    print(f"请求1完成,响应长度:{len(resp1.text)}")
    resp2 = requests.get("https://www.github.com")
    print(f"请求2完成,响应长度:{len(resp2.text)}")
    print(f"总耗时:{time.time() - start_time:.2f}秒")

if __name__ == "__main__":
    sync_blocking()
# 输出:先等请求1完成,再等请求2完成,总耗时≈2秒(两次IO等待累加)
1.4.4.2、同步非阻塞(Synchronous Non-blocking)

调用方发起任务后,等待结果(同步),但不被挂起(非阻塞),可继续做其他事,需主动轮询任务是否完成

select.select(rlist, wlist, xlist, timeout=None)单线程同时监控多个文件描述符FD的可读/可写/异常状态,避免对未就绪的FD做阻塞操作
rlist: 待监控"是否可读"的文件描述符列表(如文件、Socket、管道)
wlist: 待监控"是否可写"的文件描述符列表
xlist: 待监控"是否发生异常"的文件描述符列表
timeout: 超时时间(秒),None 表示无限等待,直到有描述符就绪
返回值:
可读列表readable:包含已就绪可读的 FD(可安全读取数据,不会阻塞)
可写列表writable:包含已就绪可写的 FD(可安全写入数据,不会阻塞)
异常列表exceptional:包含发生异常的FD(需处理错误)
import os
import time
import select
def sync_non_blocking_file_read(file_path):
    # 1. 以非阻塞模式打开文件(核心:O_NONBLOCK)
    fd = os.open(file_path, os.O_RDONLY | os.O_NONBLOCK)
    buffer = b""  # 存储读取的内容
    start_time = time.time()

    print("=== 主线程开始执行 ===")
    while True:
        # 2. 轮询检查文件描述符是否“可读”(同步: 主动等待就绪)
        readable, _, _ = select.select([fd], [], [], 0.1)  # 检查文件是否可读,超时0.1秒

        if fd in readable:   # 文件已就绪(FD变为可读)
            # 3. 非阻塞读取: 能读多少读多少,不会挂起
            try:
                chunk = os.read(fd, 1024)  # 每次读1024字节
                if not chunk:  # 读取完毕
                    break
                buffer += chunk
                print(f"读取到 {len(chunk)} 字节,累计 {len(buffer)} 字节")
            except BlockingIOError:
                # 非阻塞模式下,无数据时抛出该异常(正常)
                pass
        else:
            # 4. 非阻塞期间,主线程可执行其他任务
            print(f"文件未就绪,主线程执行其他任务(已耗时 {time.time() - start_time:.2f} 秒)")
            time.sleep(0.2)  # 模拟其他任务耗时

    # 5. 同步处理结果:读取完成后,主线程亲自处理
    os.close(fd)
    print(f"\n=== 读取完成 ===")
    print(f"文件总大小:{len(buffer)} 字节")
    print(f"总耗时:{time.time() - start_time:.2f} 秒")
    return buffer

# 测试:创建一个大文件(先执行一次,生成测试文件)
def create_large_file(file_path, size_mb=10):
    with open(file_path, "wb") as f:
        f.write(b"x" * (size_mb * 1024 * 1024))

if __name__ == "__main__":
    test_file = "large_file.bin"
    # 首次运行时创建测试文件(10MB)
    if not os.path.exists(test_file):
        create_large_file(test_file)

    # 执行同步非阻塞读取
    sync_non_blocking_file_read(test_file)
1.4.4.3、异步阻塞(Asynchronous Blocking)

逻辑矛盾但实际存在的特殊场景:调用方发起异步任务后,主动调用 “阻塞式获取结果” 的接口,导致自身挂起(阻塞),直到异步任务完成
典型场景: Python asyncio.run(),阻塞主线程直到协程完成

import asyncio
import aiohttp
import time

async def fetch_url(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

def async_blocking():
    start_time = time.time()
    # 发起异步任务
    loop = asyncio.get_event_loop()
    task1 = loop.create_task(fetch_url("https://www.baidu.com"))
    task2 = loop.create_task(fetch_url("https://www.github.com"))

    # 阻塞等待全部异步任务完成(同步等待,导致主线程挂起)
    loop.run_until_complete(asyncio.gather(task1, task2))
    
    print(f"任务1响应长度:{len(task1.result())}")
    print(f"任务2响应长度:{len(task2.result())}")
    print(f"总耗时:{time.time() - start_time:.2f}秒")

if __name__ == "__main__":
    async_blocking()
# 输出:主线程被run_until_complete阻塞,直到两个异步任务完成,总耗时≈1秒
1.4.4.4、异步非阻塞(Asynchronous Non-blocking)

调用方发起任务后,不等待结果(异步),也不被挂起(非阻塞),任务完成后由回调/事件通知调用方处理结果,是效率最高的IO模型。
典型场景: python 协程

import asyncio
import aiohttp
import time

async def fetch_url(url, callback):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            result = await resp.text()
            # 任务完成后触发回调(异步通知)
            callback(url, result)

def handle_result(url, result):
    print(f"{url} 响应长度:{len(result)}")

async def async_non_blocking():
    # 发起异步任务,不阻塞主线程
    task1 = asyncio.create_task(fetch_url("https://www.baidu.com", handle_result))
    task2 = asyncio.create_task(fetch_url("https://www.github.com", handle_result))

    # 主线程继续执行其他任务(非阻塞)
    print("主线程执行其他任务...")
    await asyncio.sleep(0.5)  # 模拟其他任务
    print("主线程其他任务完成")

    # 等待异步任务结束(非必须,仅为演示)
    await asyncio.gather(task1, task2)

if __name__ == "__main__":
    start_time = time.time()
    asyncio.run(async_non_blocking())
    print(f"总耗时:{time.time() - start_time:.2f}秒")
# 输出:
# 主线程执行其他任务...
# 主线程其他任务完成
# https://www.baidu.com 响应长度:xxx
# https://www.github.com 响应长度:xxx
# 总耗时≈1秒
# 常见误区
异步=非阻塞: 异步是 "结果处理方式",非阻塞是 "调用方状态",异步也可阻塞(如asyncio.run())
非阻塞一定高效: 同步非阻塞的轮询会消耗CPU,效率不如异步非阻塞
异步非阻塞无需等待: 只是"调用方不主动等",任务本身仍需时间执行,最终需保证任务完成(如await)
同步阻塞一无是处: 简单场景下,同步阻塞代码可读性最高,维护成本最低,无需过度优化

# 实战选择原则:
优先同步阻塞:简单任务、低并发、无性能要求(如脚本、工具类程序)
同步非阻塞慎用:仅适合短任务、需手动控制状态的场景,避免轮询消耗 CPU
异步阻塞过渡用:异步改造的中间阶段(兼容同步代码),批量处理异步任务
异步非阻塞首选:高并发 IO 密集型场景(后端服务、高吞吐接口),追求极致性能

二、协程常用类库

2.1、生成器协程

Python 早期没有原生协程,基于生成器yield/yield from实现协程功能,
生成器协程是 Python 基于生成器(Generator) 实现的轻量级协程方案,核心是利用 yield 实现暂停 / 恢复执行、数据双向传递,属于 伪协程。是 asyncio 原生协程出现前的主流协程方式

# 消费者
def consumer():
    r = ''
    while True:
        """
        执行到yield会暂停,将send过来的数据赋值给n,再继续往下走,等再次进入循环后再次暂停,等下次send传数据过来再次恢复,
        再次将send过来的数据赋值给n
        """
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

# 生产者
def producer(cons):
    cons.send(None)  # 启动生成器,到yield r处暂停,第一次直接返回r的初始值 '',并接受下次send过来的值赋值给n
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        """
        向暂停的yield表达式中传递值n,n成为yield r表达式的返回值,赋值给n,生成器恢复执行
        """
        r = cons.send(n) # 发送数据,并接受消费者处理后的数据
        print('[PRODUCER] Consumer return: %s' % r)
    cons.close()

if __name__ == '__main__':
    c = consumer()
    producer(c)

2.1.1、简单的生成器协程

def simple_coroutine():
    print("Coroutine Started")
    x = yield
    print("Coroutine received: ", x)
    y = yield x + 1
    print("Coroutine Finally received: ", y)
    return "End"

if __name__ == "__main__":
    func_return = ""
    # 创建协程对象
    cr = simple_coroutine()

    # 启动协程 等同于 next(cr)/cr.__next__()
    cr.send(None)  # 执行到第一个yield处暂停,返回None。等待下一次send()发送数据

    # 向协程发送数据
    cr.send(10)  # 协程继续执行,将10赋值给x,打印"Coroutine received: 10",然后暂停在第二个yield处,返回x+1=11,等待下一次send()发送数据

    # 再次向协程发送数据,最后一次send时,后续没有下一个yield,会抛出StopIteration异常,需要捕获,否则数据发送不成功
    try:
        cr.send(20)  # 协程继续执行,将20赋值给y,打印"Coroutine Finally received: 20",然后抛出StopIteration异常,异常的value属性为"End"
    except StopIteration as e:
        func_return = e.value
        print(f"协程返回值: {func_return}")  # 输出"协程返回值: End"

2.1.2、yield from 委托生成器

def sub_coroutine():
    total = 0
    while True:
        n = yield total # 接收传入的值,并返回累计总和
        if n is None:
            break
        total += n
    return total

def delegate_coroutine():
    res = yield from sub_coroutine()
    print("子生成器返回值: ", res)
    return f"委托协程结束, 总和: {res}"

if __name__ == '__main__':
    gen = delegate_coroutine()

    gen.send(None) # 启动协程, 执行到第一个yield,暂停,等待send值

    print(gen.send(5))  # 恢复执行,从第一次循环的yield处继续,把yield 5的返回值5赋值给n。total=5,然后进入下次循环的yield处暂停
    print(gen.send(10)) # 恢复执行,从第二次循环的yield处继续,把yield 10的返回值10赋值给n。total=5+10,然后进入下次循环的yield处暂停

    # 发送None终止子生成器,触发返回值
    try:
        gen.send(None)
    except StopIteration as e:
        print(f"最终结果:{e.value}")  # 输出:最终结果:委托协程结束,总和:15

2.1.3、基于Python的生成器的yield和yield form关键字实现协程代码

def func1():
    yield 1
    yield from func2()
    yield 2


def func2():
    yield 3
    yield 4


f1 = func1()
for item in f1:
    print(item)

2.2、greenlet

是轻量级底层协程库,核心作用是提供显式的用户态协程切换能力 ——它不包含事件循环、异步IO等高层功能,仅专注于协程的创建、暂停和恢复,是很多上层协程框架(如gevent)的底层依赖。
简单说:greenlet 是协程的基础组件,而非完整的异步解决方案,需手动控制协程切换逻辑(或依赖上层框架封装)。

'''
# 需要手动安装:python -m pip install greenlet
greenlet 是一个由C语言实现的协程模块,通过设置swith()来实现任意函数之间的切换
1. greenlet需要手动切换,当遇到IO操作时,程序会阻塞,不能进行自动切换  
'''
from greenlet import greenlet 

my_list = []
def test_switch_1():
    for i in range(1, 4):
        my_list.append(i)
        g2.switch()  # 每次for循环在g1中执行完一个循环后,就切换到g2中执行一个循环
        my_list.append(-i)  # g2切换回来后,继续执行,所以会把-i添加到列表中; 和yield类似,会记录上一次的执行现场,下次再切换回来接着执行
        g2.switch()  # 再次切换到g2中执行

def test_switch_2():
    for i in range(4, 8):
        my_list.append(greenlet.getcurrent().switch(i))
        g1.switch()  # 每次for循环在g2中执行完一个循环后,就切换到g1中执行一个循环

if __name__ == "__main__":
    g1 = greenlet(test_switch_1)  # 一个greenlet实例对应一个函数,返回一个线程对象
    g2 = greenlet(test_switch_2)
    g1.switch()
    print(my_list)  # [1, 4, -1, 5, 2, 6, -2, 7, 3]

2.3、gevent库

是一款 成熟、易用的第三方协程框架,核心基于 greenlet(底层轻量级协程切换)和 libev/libuv(高效事件循环),通过猴子补丁(Monkey Patch)实现 同步代码零修改转异步,是IO密集型任务(如爬虫、API 服务、数据库操作)的高效解决方案。

geventgreenlet 的上层封装—— 它解决了 greenlet 需手动切换协程的痛点,自动在 IO 等待时切换协程,让开发者用同步代码的写法,获得异步并发的性能

'''
需要手动安装:python -m pip install gevent
gevent遇到IO操作时,会进行自动切换 ,属于主动式切换
'''
# 常用方法:
* gevent.spawn(func, *args, **kwargs): 创建并启动一个协程(封装的greenlet实例),非阻塞执行(异步执行)
  - func: 协程异步执行的目标函数
  - *args/**kwargs: 传递给 func 的参数
  - 返回gevent.Greenlet实例,通过get()获取目标函数的返回值(阻塞/超时都能获取到)

* gevent.spawn_later(seconds, func, *args, **kwargs): 延迟 seconds 秒后启动协程(类似定时任务)
  - seconds:延迟秒数(支持浮点数)
  - 其余同 spawn

* gevent.Greenlet(func, *args, **kwargs): 手动创建Greenlet 类实例(spawn是其快捷方式),需手动调用 start() 启动
  - 核心方法:
    - start():启动协程
    - join(timeout=None):等待协程完成(可选超时时间)
    - kill(exc=GreenletExit, block=True, timeout=None):终止协程
    - get(block=True, timeout=None):获取协程返回值(阻塞/超时都能获取到),若协程抛出异常,此处会重新抛出

* gevent.sleep(seconds=0, ref=True): 非阻塞睡眠,触发协程切换(当前协程暂停,调度其他就绪协程)
  - seconds:睡眠时长(秒),默认 0(立即切换一次)
  - ref:是否阻止主循环退出(默认True- True 休眠时事件循环不退出
    - False 睡眠时主循环可退出    

* gevent.wait(objects=None, timeout=None): 等待多个协程/Event等对象完成,返回一个含有"完成"/"未完成"Greenlet对象的列表,比joinall灵活,joinall是对wait的封装
  - objects:要等待的对象列表(默认等待所有活跃协程)
  - timeout:超时时间(秒),超时判断不是严格截断,而是等待 N 秒后检查协程状态
    - 如果协程任务的延迟恰好等于N秒,且在gevent检查状态前刚好执行完(比如Task延迟2秒,超时2秒),则会被判定为"完成"
    - 只有当协程任务的延迟大于N秒,或在N秒内未执行完时,才会被判定为"未完成(pending)"

* gevent.with_timeout(seconds, func, *args, **kwargs): 为函数 / 协程设置超时时间,超时则抛出gevent.Timeout异常
  - seconds:超时时间(秒)
  - func:要执行的函数/协程

* gevent.kill(greenlet, exception=GreenletExit, block=True, timeout=None):终止指定协程,可指定终止时抛出的异常
  - greenlet:要终止的Greenlet实例
  - exception:终止时抛出的异常(默认GreenletExit,协程安静退出)
  - block:是否阻塞等待协程终止
  - timeout:阻塞等待的超时时间

* gevent.killall(greenlets, exception=GreenletExit, block=True, timeout=None):批量终止多个协程
  - greenlets:要终止的协程列表,其他参数同kill

* greenlet.join(timeout=N, raise_error=False): 阻塞当前执行流程(主线程/主协程),直到目标协程完成或超时
  - 实质上是Greenlet.join(),spawn方法会返回一个Greenlet实例。一般单个协程时使用
  - 若不调用 join,主线程可能提前退出,导致协程未执行完
  - 到timeout=N时间后仅终止主进程等待行为,不会主动终止协程;若需终止超时协程,需手动调用gevent.kill()
  - raise_error默认False。若为True,协程执行中抛出的异常会在join时重新抛出;否则异常被静默捕获
   
* gevent.joinall(greenlets, timeout=None, raise_error=False, count=None): 批量等待多个协程完成,阻塞当前线程,直到所有协程结束或超时,替代多次join()
  - greenlets:Greenlet实例列表(如[spawn(...) for ...]- timeout:全局超时时间(秒)(所有协程的总等待时间),未完成的协程会被终止
  - raise_error:是否抛出协程异常(默认 False,不抛出)
  - count:仅等待前count个协程完成,None 表示全部

* Monkey Patching: "猴子补丁"核心方法,替换标准库的同步IO模块为gevent异步实现,让同步代码零修改支持协程,否则协程无法被正确调度
  - 必须在导入其他模块(如requests、socket)之前调用!
  -  方法:
    gevent.monkey.patch_all(**kwargs)
    gevent.patchXXX()
  -  参数举例:
    -  socket=True:替换 socket(网络请求核心,默认 True)。
    -  time=True:替换 time(time.sleep 变为非阻塞,默认 True)。
    -  thread=True:替换 threading(线程操作适配协程,默认 True)。
    -  dns=True:异步 DNS 解析(默认 False,需手动开启)   
  # 猴子补丁主要作用:    
  1. 使用猴子补丁的方式,gevent能够修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。
  2. 通过猴子补丁的monkey.patch_xxx()来将python标准库中模块或函数改成gevent中响应的具有协程的协作式对象。这样在不改变原有代码的情况下,将应用的阻塞式方法,变成协程式的
  
* gevent.getcurrent(): 获取当前正在执行的Greenlet协程实例
  
# 获取任务返回值
* g.value: 获取协程执行任务的返回值,只读属性,仅用于存储执行结果,无等待逻辑
  - 当Greenlet未执行完成时,g.value = None
  - 当Greenlet执行完成(正常返回),g.value = 目标函数的返回值
  - 当Greenlet执行出错,g.value 仍为 None,异常会被捕获并存储在 g.exception 中
  - g.value不是“实时结果”,只有Greenlet执行完成后,value才会被赋值为返回值,执行中始终是 None
* g.get():带 “等待 + 超时 + 异常透传” 的结果读取
  - 调用g.get(timeout=N) 时,会先调用g.join(timeout=N) 阻塞等待
  - 若超时(N秒内任务未完成),抛出greenlet.Timeout异常
  - 若未超时(任务完成),返回g.value,且如果任务执行出错,get()会主动抛出异常
  - join(timeout)/get(timeout)的超时仅控制 “主协程等待的时长”,不会中断Greenlet本身的执行

# Greenlet终止方式:join()/get()的超时不会中断它,超时只终止主进程的等待行为
  - 目标函数执行完毕
  - 主动抛出未捕获的异常
  - 进程/线程退出
  
# 协程状态判断:
* g.ready():返回 True 表示协程已执行完毕(无论成功 / 失败)
* g.successful():返回 True 表示协程无异常完成
* g.dead: 返回 True 表示协程已终止(包括正常结束 / 被杀死)

# 2. 关键特性
- 猴子补丁(Monkey Patching):自动替换 Python 标准库中的同步 IO 模块(如socket、threading、time、select)为异步实现,原有同步代码无需改动即可异步执行
- 自动协程切换:基于事件循环(libev/libuv),当某个协程遇到IO等待(如网络请求、睡眠)时,自动切换到其他就绪协程,避免线程阻塞
- 轻量级高并发:基于greenlet实现协程,单个协程内存占用极低(远低于线程),支持数万级并发(甚至数十万)

猴子补丁(monkey patching)

方法功能示例
patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, httplib=False, subprocess=False, sys=False, aggressive=False)批量替换所有支持的标准库模块为非阻塞版本gevent.monkey.patch_all()
patch_socket()仅替换 socket 模块(网络 IO 核心)gevent.monkey.patch_socket()
patch_time()替换 time.sleepgevent.sleep(协程睡眠)gevent.monkey.patch_time()
patch_thread()替换 threading 模块为 gevent 兼容版本gevent.monkey.patch_thread()
patch_ssl()替换 ssl 模块,支持非阻塞 SSL/TLSgevent.monkey.patch_ssl()
is_module_patched(module_name)检查指定模块是否已被补丁gevent.monkey.is_module_patched(‘socket’)
undo_patch(module_name)撤销对指定模块的补丁(仅部分模块支持)gevent.monkey.undo_patch(‘time’)

2.3.1、简单案例:join/joinall+spawn/spawn_later

"""
当函数中遇到IO等操作的时候,函数就会阻塞,等待数据的返回
但使用协程时,协程不会阻塞等待,而是切换到另一个协程继续操作
下面案例当task1阻塞时,立马切换到另一个协程执行task2函数,最后根据阻塞时间,时间少的先返回
"""
def task_1():
    print("Task 1 started")
    gevent.sleep(4)  # 非阻塞睡眠,触发协程切换
    print("Task 1 completed")

def task_2():
    print("Task 2 started")
    gevent.sleep(3) # 非阻塞睡眠,触发协程切换
    print("Task 2 completed")

if __name__ == '__main__':
    g1 = gevent.spawn(task_1)
    g2 = gevent.spawn(task_2)

    """
    两个任务同时执行,谁耗时少,谁先结束
    1、join() 阻塞主协程,等待目标协程执行完成
    2、一般有多少子协程,就写多少join()
    3、可以用joinall()代替join()
    4、不调用join()/joinall,子协程不会执行
    5、timeout参数说明:
    表示仅终止等待行为,不会主动终止线程,以该例说明:
    (1)g1.join(timeout=N):表示等到协程1执行结束,等到超时时间为N秒
    (2)task_1阻塞耗时4秒,g1等待协程完成的超时时间是2秒,则当task_1运行到sleep阻塞后,等待2秒后就不再等待该协程任务完成,所以
    "Task 1 completed"不会打印。而是会主动切换到task_2任务,等待task_2任务执行完成。注意:g1协程用时2秒
    (3)task_2任务阻塞时间是3秒,g2等待协程完成的超时时间是1秒。那么此时总等待超时时间就是:g1+g2=3秒,因为是非阻塞睡眠。刚好,task_2的阻塞时间正好是3秒,
    所以task_2阻塞结束,任务可以执行完成
    (4)要想所有任务都执行完成,则每个任务的等待超时时间一定要大于阻塞时间.或者使用joinall()设置一个统一的超时时间(所有协程阻塞时间总和)
    (5)过了超时时间不会终止协程,只是终止等待,如果要终止协程,使用gevent.kill(g1)
    """
    # g1.join(timeout=2)
    # g2.join(timeout=1)
    gevent.joinall([g1, g2],timeout=5)
    print("All tasks completed")
    
 --------------------------------------------------------------------------------------------

import gevent
from gevent import spawn_later
"""
spawn_later: 
类似于定时任务,指定多长时间后开始启动协程,执行目标函数,指定的时间可以是浮点数,其他功能和spawn一样
"""
def task(name, delay):
    print("协程: %s, 阻塞时间: %s" %(name, delay))
    gevent.sleep(delay)
    print(f"协程: {name} Done")
    return name

if __name__ == '__main__':
    # 1. 创建协程
    g_list = []
    # g_list = [spawn_later(2.2, task, name, times+1) for times, name in enumerate(['A','B','C'])]
    for times, name in enumerate(['A','B','C']):
        g = spawn_later(5.5, task, name, times+1) # 5.5秒后开始执行task函数
        g.task_name = name
        g_list.append(g)

    gevent.joinall(g_list)
    print("主协程结束")

2.3.2、简单案例:wait/with_timeout

# wait
import gevent

def task_1(name, delay):
    print(f"Task {name} started(延迟 {delay} 秒)")
    gevent.sleep(delay)
    print(f"Task {name} Finished")
    return name + "-OK"

if __name__ == '__main__':
    # 参数times为1, 2, 3
    g_list = []
    # g_list = [gevent.spawn(task_1, name, times+1) for times, name in enumerate(['A', 'B', 'C'])]  # 创建协程对象
    for times, name in enumerate(['A协程', 'B协程', 'C协程']):
        g = gevent.spawn(task_1, name, times+1)
        g.task_name = name  # 自定义
        g_list.append(g)

    gevent.wait(g_list, timeout=2.5)  # 等待所有任务完成或超时,仅返回在超时前已完成(ready) 的协程对象,未完成的协程不会出现在返回值里
    
    # 获取任务的返回值
    for g in g_list:
        # 只获取到两个,"协程C" 没执行,所以返回值为None
        print(f"协程 {g.task_name} 的返回值: {g.value}")  

    success = [g for g in g_list if g.successful()]  # 已完成(正常结束)
    failed = [g for g in g_list if g.ready() and not g.successful()]  # 已完成(异常结束/)
    pending = [g for g in g_list if not g.ready()]  # 未完成(超时)

    print("正常完成: ", len(success))
    print("异常完成: ", len(failed))
    print("未完成: ", len(pending))

    for g in pending:
        # g.kill()  # 终止未完成的协程
        gevent.kill(g) # 两种写法都可以
        print("被kill的协程名称:{}, 状态: {}".format(g.task_name, '已死亡' if g.dead else '未死亡'))
        
 -----------------------------------------------------------------------------------------------

# with_timeout
def task_1(name, delay):
    print(f"Task {name} started(延迟 {delay} 秒)")
    gevent.sleep(delay)
    print(f"Task {name} Finished")
if __name__ == '__main__':
    try:
        """
        第一个参数设置任务执行的超时时间。
        超时时间 > 非阻塞睡眠时间,则不会报错。如果小于,则会报超时异常
        """
        done= gevent.with_timeout(2, task_1, 'D', 2)
    except gevent.Timeout as ex:
        print("任务超时: ", str(ex))

2.3.3、简单案例:Greenlet/g.get()/g.value

g1 完整执行路径(含时间轴)

时间节点执行主体具体行为状态 / 结果
0ms主协程创建 g1 = Greenlet(task, "Greenlet1", 4) → 调用 g1.start() 启动 g1g1 进入「待调度」状态,主协程继续执行
0ms主协程调用 g1.join(timeout=1),主协程阻塞等待 g1 完成gevent 调度器切换到 g1 执行
0msg1 子协程执行 task 函数 → 打印 协程: Greenlet1, 阻塞时间: 4 → 调用 gevent.sleep(4)g1 主动让出 CPU,回到「阻塞」状态,调度器切回主协程
1000ms主协程g1.join(timeout=1) 超时(已等 1 秒,g1 仍阻塞)→ 主协程继续向下执行主协程累计等待 g1 1 秒,g1 还需阻塞 3 秒
1000ms主协程进入 try 块 → 调用 g1.get(timeout=1)(内部先调用 g1.join(timeout=1)主协程再次阻塞等待 g1 完成
2000ms主协程g1.get(timeout=1) 超时 → 抛出 gevent.Timeout 异常异常被 except 捕获,主协程不终止,继续向下执行
2000ms主协程创建 g2 = Greenlet(task, "Greenlet2", 2) → 调用 g2.start() 启动 g2g2 进入「待调度」状态
2000ms主协程立即调用 g2.kill() → 强制终止 g2 → 打印 线程2状态:已死亡g2 被标记为 dead,未执行任何 task 内逻辑
2000ms主协程调用 gevent.sleep(2),主协程主动让出 CPU 并阻塞 2 秒调度器切换到 g1 执行
2000msg1 子协程gevent.sleep(4) 中恢复(已阻塞 2 秒,还需 2 秒)→ 继续阻塞g1 仍处于「阻塞」状态
4000msg1 子协程gevent.sleep(4) 阻塞完成 → 打印 协程: Greenlet1 Done → 返回 "Greenlet1"g1 执行完成,g1.value 被赋值为 "Greenlet1",g1 进入「完成」状态
4000ms主协程gevent.sleep(2) 阻塞完成 → 打印 最终g1的返回值:Greenlet1主协程执行完毕
4000ms+主进程主线程执行完毕 → 主进程退出所有资源被回收,程序结束

g2 完整执行路径(含时间轴)

时间节点执行主体具体行为状态 / 结果
2000ms主协程创建 g2 = Greenlet(task, "Greenlet2", 2) → 调用 g2.start() 启动 g2g2 进入「待调度」状态(尚未执行 task 任何逻辑)
2000ms主协程启动 g2 后立即调用 g2.kill()gevent 调度器强制终止 g2,标记 g2.dead = True
2000ms主协程执行 print("线程2状态:已死亡")打印结果,g2 无任何 task 内逻辑执行
2000ms+g2 子协程始终处于「已死亡」状态无任何输出,无返回值(g2.value = None
"""
关键问题:
为啥 get() 超时后,任务还能执行完成?
核心原因是:
Greenlet 的 join()/get() 超时仅控制 “等待方的阻塞时长”,不影响被等待的 Greenlet 本身的执行状态。
拆解逻辑:
Greenlet启动后(g.start()),会进入独立的执行流,其生命周期不受 join()/get() 控制;
g.get(timeout=N) 本质是 “我(调用方)最多等 N 秒,等你(Greenlet)执行完”—— 超时只是 “我不等了”,但你(Greenlet)仍会继续执行;
Greenlet 只有两种终止方式:① 目标函数执行完毕;② 主动抛出未捕获的异常;③ 进程 / 线程退出。join()/get() 的超时不会中断它。
类比理解:
你让同事帮你处理一个 3 分钟的任务,你说 “我最多等你 2 分钟,等不及我就先去忙别的”——2 分钟后你不等了,但同事仍会继续处理完这个任务,只是你没等到结果而已。
"""
import gevent
from gevent import Greenlet

def task(name, delay):
    print("协程: %s, 阻塞时间: %s" %(name, delay))
    gevent.sleep(delay)
    print(f"协程: {name} Done")
    return name

if __name__ == '__main__':
    g1 = Greenlet(task, "Greenlet1", 4)
    g1.start()

    g1.join(timeout=1) # 超时时间1秒,表示主协程最多等待1秒, 如果1秒内g1执行完,则主协程继续,否则主协程继续向下执行

    # print("协程1返回值:", g1.value)

    """
    # get()方法源码:
    def get(self, timeout=None):
        self.join(timeout=timeout)  # 先等待 Greenlet 完成(支持超时)
        if self.exception is not None:
           raise self.exception  # 透传执行异常
           return self.value  # 完成后返回结果
    """
    """
    对g.get(timeout=N) 关键说明:
    1、get(timeout=N)方法是等待g1执行完成,源码中实际也会调用join(timeout=N)
    2、timeout=N,表示等待N秒后,则主协程不再等待,继续向下执行。该例中,在调用get(timeout=1)之前已经调用了g1.join(timeout=1),
      所以一共等待了 1(join) + 1(get)= 2秒。而g1的task需要阻塞4秒(gevent.sleep(4)),2秒 < 4秒,因此get必然触发超时。
      所以此时会抛出超时gevent.Timeout异常,但是协程任务不会终止
    3、若没有try捕获超时异常,则异常会向上传播,导致整个进程终止,最终会导致协程g1终止执行
    4、如果捕获了超时异常,又有两种情况:
      (1)若捕获了异常,协程虽然不会终止,但是还没等到阻塞结束之后任务执行完成,主协程在剩下的2秒内可能就已经执行完了,触发主进程终止,
          进程退出时强制终止所有协程,导致g1协程也终止,print()甚至没被执行到,所以啥都不打印
          主协程如果不加gevent.sleep(3),分析一下主协程中执行情况:
          此时g1还需要至少等待2秒才能完成,但g2可能已经使用1秒时间执行完成了,那么此时还需要至少1秒,g1才能执行完成。但是主进程已经触发了终止,协程也会被强制终止,所以g1也被终止,所以可能try中内容没来得及执行
      (2)在主协程中添加非阻塞等待时间来等待g1完成。gevent.sleep(3)
          非阻塞时间的设置:如果代码中没有其他耗时操作,
          那么 sleep(3)+ 1(join) + 1(get) 要大于等于 任务中的阻塞时间 4,否则g1也执行不完成      
    易错点:    
    1、主协程的 gevent.sleep(2) 不是“叠加等待时间”(不是sleep(4)+sleep(2)=sleep(6)),而是“让出 CPU 并阻塞自身”——这段时间内g1会持续执行,而非主协程“等够时间后再让g1执行(等阻塞时间到4后,g1继续执行)”;
    2、g1的总阻塞时间是4秒,主协程的 join(1) + get(1) 累计等待2秒,剩余 2 秒刚好由gevent.sleep(2)覆盖,因此g1能完整执行;若sleep时间小于2秒(如1秒),g1会因主进程退出被强制终止;
    3、g2的kill()是“主动终止”,与g1因“主进程退出被动终止”有本质区别——kill()是gevent层面的协程终止,而被动终止是操作系统层面的进程资源回收
    """
    try:
        print("协程1返回值:", g1.get(timeout=1))
    except gevent.Timeout:
        pass

    g2 = Greenlet(task, "Greenlet2", 2)
    g2.start()
    g2.kill()  # 终止协程, 主动终止g2,无论是否执行中都会立即标记为dead,且g2的task逻辑会被中断(大概率来不及打印)
    print("线程2状态:", '已死亡' if g2.dead else '未死亡')

    # 关键:主协程阻塞3秒,让g1完成剩余的4-2=2秒阻塞 + 后续打印/返回逻辑
    gevent.sleep(2)
    print("最终g1的返回值:", g1.value)  # 此时打印 "Greenlet1"

2.3.4、简单案例:monkey.patch_all/patch_socket 猴子补丁

'''
monkey.patch_all()对zope.event,zope.interface有版本要求
gevent版本	兼容的zope.event版本	兼容的zope.interface版本
21.12.0	       4.3 ~ 4.6	           5.0 ~ 5.5.2
22.0.0+	       4.6 ~ 5.0	           5.5.2 ~ 6.0
23.0.0+	       5.0 ~ 6.1	           6.0 ~ 8.1.1
否则patch_all()会报错 
“pkg_resources.DistributionNotFound: The 'zope.event' distribution was not found....”
'''    
from gevent import monkey
monkey.patch_all()  # 会将requests等标准库中的阻塞操作变成非阻塞操作
import gevent
import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

def fetch(url):
    print(f"Fetching {url}")
    resp = requests.get(url, verify= False) # 当有IO操作,程序发送阻塞时,会自动切换到其他协程执行
    print(f"Done Fetching {url}, Status Code:{resp.status_code}")

if __name__ == '__main__':
    urls = ['https://www.baidu.com', 'https://www.python.org']
    jobs = [gevent.spawn(fetch, url) for url in urls]
    gevent.joinall(jobs)
import socket
print("原始socket类型: ", socket.socket)
print("After monkey patch")

from gevent import monkey
monkey.patch_socket()
print("gevent socket类型: ", socket.socket)
print('-' * 40)
import select
print("原始select类型: ", select.select)
print("After monkey patch")
monkey.patch_select()
print("gevent select类型: ", select.select)

# 输出结果: 
"""
原始socket类型:  <class 'socket.socket'>
After monkey patch
gevent socket类型:  <class 'gevent._socket3.socket'>
----------------------------------------
原始select类型:  <built-in function select>
After monkey patch
gevent select类型:  <function select at 0x000001D9C4B12CB0>
"""
from gevent import monkey
monkey.patch_all()
import gevent
import time
"""
加上猴子补丁之后,time.sleep()的作用跟gevent.sleep()作用一样
当前任务如果阻塞时会自动切换到下一个协程执行目标函数
"""
def printf1(num):
    for i in range(num):
        print("这是第", i, gevent.getcurrent())
        time.sleep(1)

def printf2(num):
    for i in range(num):
        print("这是第", i, gevent.getcurrent())
        time.sleep(1)

g1 = gevent.spawn(printf1, 2)
g2 = gevent.spawn(printf2, 2)
g1.join()
g2.join()
"""
# 不加猴子补丁,就是普通的同步执行,等printf1执行完正确返回后再执行printf2
这是第 0 <Greenlet at 0x20443a38160: printf1(2)>
这是第 1 <Greenlet at 0x20443a38160: printf1(2)>
这是第 0 <Greenlet at 0x20443a38040: printf2(2)>
这是第 1 <Greenlet at 0x20443a38040: printf2(2)>
# 加了猴子补丁
这是第 0 <Greenlet at 0x1c0746f1598: printf1(2)>
这是第 0 <Greenlet at 0x1c0746f17b8: printf2(2)>
这是第 1 <Greenlet at 0x1c0746f1598: printf1(2)>
这是第 1 <Greenlet at 0x1c0746f17b8: printf2(2)>
"""

2.4、asyncio库

asyncio库的详细解释

三、协程间同步

3.1、gevent 协程间同步

3.1.1、Lock 互斥锁

# Lock锁的作用
当多个协程需要访问同一个资源时,避免访问的冲突。加锁保证了多个协程访问同一块数据时,同一时间只有一个协程执行临界区代码。Lock包含两种状态,锁定和非锁定

# 构造方法:
gevent.lock.Lock()

# 实例方法:
* acquire([timeout]): 获取锁,使协程进入同步阻塞状态,尝试获得锁定
* release(): 释放锁,使用前协程必须获得锁定,否则将抛出异常
    
* with Lock(): 自动获取/释放锁
3.1.1.1、Lock() 简单案例
import gevent
from gevent.lock import Lock
"""
# 推荐使用with Lock
with lock:
    current = counter
    gevent.sleep(0)
    counter = current + 1
"""
number = 0
lock = Lock()

def counter(name, num):
    global number
    for _ in range(num):
        lock.accquire()  # 获取
        try:
            """
            为什么不直接写 counter += 1?
            counter += 1在Python中看似是“一步操作”,但底层其实是读取-修改-写入三步(先读counter,加 1,再写回)
            这三步拆开来写,并且在中间插入 gevent.sleep(0),是为了故意制造一个 “时间窗口”,
            清晰看到:不加锁时,多个协程会同时读取到相同的number值,最终导致计数覆盖
            如果直接写counter += 1,不加锁时错误依然存在,只是概率更低(因为读取-修改-写入的时间窗口极短),而拆分后错误会100%出现
            """
            current = number  # 共享变量number的当前值读取到局部变量current中(比如此时number=0,那么current=0)
            gevent.sleep(0)  # 强制触发gevent协程调度(让当前协程暂停,切换到其他协程执行)如果不加锁,其他协程会在这一步抢走执行权,导致数据错误
            number = current +1  # 把current加1后,写回共享变量number(比如 current=0,则number变为 1)
        finally:
            lock.release()  # 释放
    print(f"协程 {name} 执行完成, 当前计数器值为:{number}")

if __name__ == "__main__":
    g_list = [gevent.spawn(counter, name, 1000) for name in ["线程A", "线程B", "线程C"]]
    gevent.joinall(g_list)
    print(f"最终计数器值:{counter}")

3.1.2、RLock 可重入锁

# RLock锁的作用
1. 同一协程可多次获取同一把锁,同时不会造成阻塞
2. 拥有RLock的协程可以再次调用accquire(),释放锁时需要调用release()相同次数
3. 只有最外面一对的release()将锁解开,才能让其他协程继续处理

# 构造方法:
gevent.lock.RLock()

# 实例方法:
* acquire([timeout]): 获取锁,使协程进入同步阻塞状态,尝试获得锁定
* release(): 释放锁,使用前协程必须获得锁定,否则将抛出异常
    
* with RLock(): 自动获取/释放锁
3.1.2.1、RLock() 简单案例
import gevent
from gevent.lock import RLock

"""在递归中使用可重入锁"""
lock = RLock()
def task(num):
    with lock:
        if num == 1:
            return 1
        return num * task(num-1)

if __name__ == '__main__':
    g = gevent.spawn(task, 3)
    g.join()
    print(g.value)
import time
from typing import Dict
import gevent
from gevent.lock import RLock

# 商品库存
inventory: Dict[str, int] = {
    "mi_17": 12,
    "iphone_13": 20
}

# 库存锁
inventory_lock: Dict[str, RLock] = {
    name: RLock() for name in inventory.keys()
}

class InventoryManager:
    @staticmethod
    def check_stock(product_name, quantity) -> bool:
        """检查商品库存"""
        # 同一协程再次获取锁(可重入特性)
        with inventory_lock[product_name]:
            current_stock = inventory[product_name]
            print(f"[校验库存] 商品 {product_name} 当前库存: {current_stock}, 请求扣减:{quantity}")
            if current_stock >= quantity:
                return True
            return False

    # TODO: 补充扣减库存逻辑
    @staticmethod
    def deduct_stock(product_name, quantity) -> bool:
        """扣减库存"""
        if quantity <= 0:
            return False
        # 第一层加锁:保证扣减操作原子性
        with inventory_lock[product_name]:
            # 嵌套调用check_stock(同一协程再次获取同一把锁,RLock允许)
            if not InventoryManager.check_stock(product_name, quantity):
                print(f"【扣减失败】 商品: {product_name} 库存不足")
                return False

            gevent.sleep(0.1) # 模拟网络延迟
            inventory[product_name] -= quantity # 扣减库存
            print(f"【扣减成功】 商品:{product_name} 剩余库存:{inventory[product_name]}")
            return True

def place_order(order_id, product_name, quantity):
    print(f"【下单请求】订单 {order_id} 开始处理,商品 {product_name},库存 {quantity}")
    start_time = time.time()
    try:
        is_success = InventoryManager.deduct_stock(product_name, quantity)
        if is_success:
            print(f"【下单成功】订单 {order_id} 处理完成,耗时:{time.time() - start_time:.2f}秒")
        else:
            print(f"【下单失败】订单 {order_id} 处理失败,耗时:{time.time() - start_time:.2f}秒")
    except Exception as e:
        print(f"【下单异常】订单 {order_id} 处理异常:{str(e)}")

if __name__ == '__main__':
    orders = [gevent.spawn(place_order, f"order_{i}", "mi_17", 2) for i in range(10)] + \
        [gevent.spawn(place_order, f"order_{i}", "iphone_13", 3) for i in range(5)]

    gevent.joinall(orders) # 等待所有订单处理完成

    # 输出最终库存
    print("\n[最终库存]")
    for sku, stock in inventory.items():
        print(f"商品{sku}: {stock}台")

3.1.3、Semaphore 信号量

"信号量"更高级的锁机制,可以限制同时执行的协程数。
如果指定信号量为 3,那么允许同时执行3个协程,每加入一个协程,信号量内部的"计数器"就会 +1,当计数器等于3时,其他的协程会阻塞,等有锁释放,其他会立即获取这个锁

# 构造方法:
gevent.lock.Semaphore([value])
* value: 设定信号量,默认值为1

# 实例方法:
* acquire([timeout]): 获取锁,使协程进入同步阻塞状态,尝试获得锁定
* release(): 释放锁,使用前协程必须获得锁定,否则将抛出异常
    
* with Semaphore(): 自动获取/释放锁
3.1.3.1、Semaphore()简单案例
import gevent
from gevent.lock import Semaphore
"""
sem.acquire()
try:
    print(f"task {index} start")
    gevent.sleep(2)
    print(f"task {index} end")
finally:
    sem.release()
"""
def test_semaphore(index, sem):
    with sem:
        print(f"task {index} start")
        gevent.sleep(2)
        print(f"task {index} end")

if __name__ == '__main__':
    semaphore = Semaphore(3)
    # 一共5个任务,但是同时只能有3个任务获得锁执行
    gevent.joinall([gevent.spawn(test_semaphore, i, semaphore) for i in range(5)])

四、协程间通信

六、协程池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位不知名民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值