python 第二十课 (协程)
协程的核心概念
协程是一种线程内部的任务调度机制,通过事件循环实现任务的挂起与恢复执行。其核心目标是在单线程内高效处理多任务,避免不必要的CPU等待。
协程与线程的关系
协程运行在线程内部,并非线程间的切换。操作系统无法感知协程的存在,它完全由程序员通过代码实现的任务切换机制控制。协程的设计初衷是减少线程切换的开销。
事件循环的作用
事件循环是协程的核心调度器,主要承担三项职责:
- 维护任务队列
- 监听I/O事件
- 管理协程的挂起与恢复
asyncio.run()是启动事件循环的标准入口点。
I/O操作识别
在协程中,任何需要等待外部响应的操作都属于I/O操作。这些操作通常以await关键字为标识:
await asyncio.sleep(1) # 时间等待型I/O
await aiohttp.get(url) # 网络请求型I/O
await file.read() # 磁盘读写型I/O
性能优势
创建子进程比创建子线程消耗更多CPU资源。协程通过以下方式优化性能:
- 单线程内任务切换无需上下文切换
- 遇到I/O时立即释放CPU资源
- 仅对I/O密集型任务有效
实现原理
当事件循环将任务交给CPU执行时,若检测到await关键字会立即挂起当前任务。待I/O操作完成后,事件循环会自动恢复该任务的执行。这种机制确保CPU资源始终被有效利用。
协程函数与协程对象
协程函数是通过async关键字修饰的函数,调用协程函数会返回一个协程对象。调用协程函数本身不会执行函数内的代码,而是生成一个待执行的协程对象。
async def work():
print('work开始工作')
print('work正在工作.......')
print('work作结束')
return '我工作结束了!!!'
协程对象的执行
协程对象需要通过事件循环(如asyncio.run())来执行。asyncio.run()会自动管理协程的执行流程,包括调用send方法并返回结果。
coroutine_obj = work()
asyncio.run(coroutine_obj)
协程与线程的区别
协程是单线程下的并发模型,通过事件循环调度任务。同一时刻CPU只执行一个线程,但协程可以在单个线程内实现任务切换,避免线程切换的开销。
关键点总结
- 协程函数需用
async定义,调用后返回协程对象。 - 协程对象需通过
asyncio.run()或事件循环触发执行。 - 协程在单线程内实现并发,效率高于多线程。
- 协程适合I/O密集型任务,避免线程阻塞问题。
协程基本概念
协程是一种轻量级的线程,由用户控制调度,在单线程内实现并发。通过 async/await 语法实现,能够高效处理 I/O 密集型任务。
协程的状态包括:
PENDING:刚创建,尚未执行。SUSPENDED:遇到await暂停执行。FINISHED:执行完成或抛出异常。
await 关键字的作用
await 用于挂起当前协程的执行,直到等待的对象完成:
- 挂起:暂停当前协程,将控制权交还给事件循环。
- 等待:事件循环执行
await后的对象:- 若对象不涉及 I/O,事件循环无法切换其他任务。
- 若对象涉及 I/O(如网络请求、文件读写),事件循环可调度其他任务。
- 恢复:对象执行完成后,事件循环恢复挂起的协程,继续执行并返回结果。
I/O 操作与协程的关系
- I/O 操作:程序与外部设备(如磁盘、网络)交换数据,会导致 CPU 等待。
- 协程优化:通过
await标记 I/O 操作点,事件循环可在等待期间切换其他协程,避免 CPU 空转。
代码示例分析
import asyncio
async def study():
print('开始学习')
await asyncio.sleep(2) # 模拟 I/O 操作
print('学习结束')
return '学习返回'
async def main():
print('main开始')
await study() # 等待 study() 协程完成
print('main结束')
return 'main返回'
res = asyncio.run(main()) # 运行事件循环
print(res)
执行流程
asyncio.run(main())启动事件循环,执行main()协程。main()打印main开始,随后await study()挂起main(),执行study()。study()打印开始学习,await asyncio.sleep(2)挂起study(),事件循环等待 2 秒。study()恢复后打印学习结束,返回学习返回。main()恢复后打印main结束,返回main返回。- 最终输出
main返回。
关键注意事项
await后仅接受可等待对象:协程对象、Task、Future或实现了__await__方法的对象。- 事件循环的调度依赖于 I/O 操作。纯计算任务(无
await)会阻塞事件循环。 asyncio.run()是高级接口,负责创建事件循环并运行协程。
异步编程核心概念
asyncio 是 Python 实现异步编程的标准库,基于协程(Coroutine)和事件循环(Event Loop)构建。以下代码演示了关键机制:
async def coro(): # 协程函数定义
await async_operation() # 挂起执行点
任务调度原理
asyncio.create_task() 将协程包装为 Task 对象并立即加入事件循环。多个任务通过 await 显式等待时,事件循环会自动调度:
task1 = asyncio.create_task(work(1, 2)) # 立即开始执行
task2 = asyncio.create_task(work(2, 2)) # 并发执行
执行流程控制
await 表达式实现非阻塞等待,当遇到 IO 操作时自动切换上下文。同步代码与异步代码的混合执行需注意:
print('同步输出') # 立即执行
await asyncio.sleep(1) # 挂起当前协程
返回值处理机制
协程返回值通过 await 获取,asyncio.run() 返回主协程的最终结果:
result = await task # 获取单个任务返回值
final_res = asyncio.run(main()) # 获取入口函数返回值
性能测量方法
使用 time.time() 计算异步操作的耗时需注意测量点的位置:
start = time.time()
await task1 # 包含在此范围内的await都会计入耗时
print(time.time() - start)
异步编程核心概念
asyncio 是 Python 的异步 I/O 框架,基于事件循环实现协程并发。示例代码展示了以下关键点:
- 协程定义:通过
async def声明协程函数,如work()和main()。 - 可等待对象:
await后接协程、Task 或 Future 对象,例如await asyncio.sleep(delay)。 - 事件循环:
asyncio.run(main())启动事件循环并执行顶层协程。
并发执行机制
asyncio.gather() 用于并发运行多个协程:
res = await asyncio.gather(work(n=1, delay=8), work(n=2, delay=4), work(n=3, delay=2))
- 所有协程并行执行,总耗时由最长延迟(8秒)决定。
- 返回值按输入顺序返回列表。
同步与异步对比
注释部分展示了同步调用方式:
res1 = await work(n=1, delay=2) # 需等待2秒
res2 = await work(n=2, delay=2) # 再等待2秒
res3 = await work(n=3, delay=2) # 又等待2秒
- 同步调用总耗时为各任务延迟之和(6秒)。
- 异步并发总耗时仅为最慢任务的延迟(8秒)。
调试与性能分析
代码通过 time.time() 记录执行时间:
start = time.time()
# ...异步操作...
print('main结束', time.time()-start) # 输出总耗时
- 验证异步并发的效率优势。
- 实际场景可用于性能基准测试。
返回值处理
协程返回值通过两种方式传递:
- 直接通过
return返回,如work()返回字符串。 asyncio.run()的返回值是顶层协程的返回结果,最终被打印。
注意事项
- 避免混用阻塞操作(如
time.sleep)与异步代码。 - 所有异步操作需在协程函数内通过
await调用。 - 事件循环由
asyncio.run()自动管理,通常无需手动创建。
异步编程与同步下载的区别
同步下载代码会阻塞当前线程,必须等待当前图片完全下载完成后才能开始下一张图片的下载。这种模式在I/O密集型任务中效率较低,因为大部分时间都浪费在等待网络响应上。
异步下载利用事件循环和非阻塞I/O操作,可以在等待一个下载完成的同时开始其他下载任务。这种模式特别适合处理大量网络请求,能显著提高程序的吞吐量。
异步编程关键知识点
事件循环(Event Loop)
- 异步程序的核心,负责调度和执行协程任务
- 通过
asyncio.run()自动创建和管理 - 可以同时监控多个I/O操作的状态
协程(Coroutine)
- 使用
async def定义的异步函数 - 通过
await表达式暂停执行,直到等待的操作完成 - 协程对象需要被事件循环调度才能执行
aiohttp库要点
ClientSession是异步HTTP客户端的主要接口- 需要配合
async with上下文管理器使用 - 响应对象的
read()方法也是异步操作,需要await
任务调度
asyncio.gather()用于并发运行多个协程- 接收多个协程对象作为参数
- 返回所有协程结果的聚合列表
代码优化建议
异常处理应该更加细致,特别是网络请求可能出现的各种错误。文件名生成方式可以改进,当前截取URL最后10个字符的方法不够可靠。可以考虑使用更完善的临时文件命名方案或保留原始文件名。
性能对比
异步版本在下载多张图片时优势明显:
- 同步版本是串行执行,总时间=各图片下载时间之和
- 异步版本是并行执行,总时间≈最慢的单个图片下载时间
- 当图片数量增加时,异步版本的优势会成倍放大
代码实现:
from idlelib.rpc import response_queue
import requests
def download(url):
print(f'开始下载')
response = requests.get(url)
print(response.content)
# 保存图片到本地
with open(url[-10:], 'wb') as f:
f.write(response.content)
print(f'下载成功')
def main():
url_list = [
'https://pic4.zhimg.com/v2-ff9deaad173974d1f0dca26b8e879299_1440w.jpg',
#'https://ts2.tc.mm.bing.net/th/id/OIP-C.5AmeaglwRvHzzdbFVQcJ-QHaNK?rs=1&pid=ImgDetMain&o=7&rm=3',
#'https://tse4.mm.cn.bing.net/th/id/OIP-C.9QuK_KvGcFEJChiNU3nCywHaLH?w=196&h=294&c=7&r=0&o=7&dpr=1.3&pid=1.7&rm=3',
]
for url in url_list:
download(url)
main()
async def download(session, url):
print(f'开始下载: {url}')
try:
# 发送异步请求
async with session.get(url) as response:
if response.status == 200:
# 读取图片数据
content = await response.read()
# 取URL最后10个字符当文件名
filename = url[-10:]
# 保存文件
with open(filename, 'wb') as f:
f.write(content)
print(f'✅ 下载成功: {filename}')
else:
print(f'❌ 下载失败,状态码: {response.status}')
except Exception as e:
print(f'⚠️ 下载出错: {e}')
async def main():
# 要下载的图片URL列表
url_list = [
'https://pic4.zhimg.com/v2-ff9deaad173974d1f0dca26b8e879299_1440w.jpg',
]
# 创建异步会话
async with aiohttp.ClientSession() as session:
# 创建协程任务列表
coroutine_list = [download(session, url) for url in url_list]
# 并发执行所有任务
await asyncio.gather(*coroutine_list)
if __name__ == '__main__':
asyncio.run(main())
# 1. 创建一个事件循环
# 2.将收到 的写成对象包装程一个任务交给时间循环
# 启动时间循环
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)