Python asyncio异步编程核心机制与实战指南
本文深入解析Python中asyncio异步编程的核心原理。从协程的概念出发,对比多线程与协程的优劣,介绍greenlet、gevent及asyncio的发展脉络。重点讲解async/await关键字的使用、事件循环机制、Task对象管理以及IO密集型场景下的性能优化策略,帮助开发者掌握高效的并发编程技巧。
1. 理解asyncio的基础:协程概念
协程的核心意义在于优化资源利用。
- 对于纯计算型操作,使用协程进行切换并无实际收益,因为保存和恢复状态反而会增加开销,降低执行效率。
- 针对IO型操作,协程的优势得以体现:在等待IO完成的间隙,程序可以切换去执行其他任务;当IO操作结束后,系统自动回调继续执行。这种机制大幅节省了系统资源并提升了性能,实现了真正的异步编程——即无需等待前一个任务结束即可执行后续代码。
2. 协程与多线程的异同分析
两者的共同点在于:
它们都属于并发操作模型。在同一时刻,多线程中只有一个线程处于执行状态,同样,协程在同一时刻也只有一个任务在执行。
主要区别如下:
多线程依赖操作系统在IO阻塞时切换线程以实现并发。切换时机由操作系统决定,开发者无需干预,但容易引发竞争条件(race condition)。
协程仅占用单个线程,在IO阻塞时通过手动切换任务来实现并发。切换时机由开发者控制,因此避免了竞争条件。此外,协程的任务切换开销远小于多线程的线程切换。从代码编写角度看,多线程并发的实现通常比协程更直观简便。
通常情况下,协程并发的处理效率高于多线程并发。
3. 使用greenlet实现协程
greenlet库用于创建协程,并通过switch函数实现协程间的切换。在某个协程执行过程中,可随时被其他协程通过switch打断,转而执行目标协程。原协程的中断现场会被保存,一旦再次获得CPU执行权,便恢复现场并从中断处继续执行。这种机制下的协程是同步执行的,无法实现真正的并发。
安装命令:pip install greenlet
import time
import greenlet
def func1():
print("func11")
gr2.switch()
time.sleep(1)
print("func22")
gr2.switch()
def func2():
print("func33")
gr1.switch()
time.sleep(1)
print("func44")
start = time.time()
gr1 = greenlet.greenlet(func1)
gr2 = greenlet.greenlet(func2)
gr1.switch()
end = time.time()
print(end - start)
4. 使用yield关键字实现协程
def func1():
yield 1
yield from func2()
yield 3
def func2():
yield 2
yield 4
ff = func1()
for item in ff:
print(item)
5. 使用gevent库实现协程
(1)gevent基础协程实现
安装命令:pip install gevent
from greenlet import greenlet
from time import sleep
def func1():
print("协程1")
sleep(2)
g2.switch()
print("协程1恢复运行")
def func2():
print("协程2")
sleep(1)
g3.switch()
def func3():
print("协程3")
sleep(1)
g1.switch()
if __name__ == '__main__':
# 使用greenlet来创建三个协程
g1 = greenlet(func1)
g2 = greenlet(func2)
g3 = greenlet(func3)
# print(g1)
g1.switch() # 让协程g1取抢占cpu资源
(2)gevent实现异步协程
# 协程被创建出来以后默认是多个协程同步执行
# 我们可以加入monkey补丁,把同步的协程转成异步协程
from gevent import monkey # 注意:monkey的引入必须在其他模块之前
monkey.patch_all() # 用monkey给整个协程队列,添加一个非阻塞I/O的补丁,使得他们成为异步协程
import time
import requests
import gevent
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'}
def func(url, i):
print("协程%d开启!" % i)
res = requests.get(url=url, headers=headers)
html = res.text
print("协程%d执行结束,获取到的响应体大小为:%d" % (i, len(html)))
if __name__ == '__main__':
start = time.time()
urls = [
"https://www.baidu.com/",
"https://www.qq.com/",
"https://www.sina.com.cn",
"https://www.ifeng.com/",
"https://www.163.com/"
]
# 创建5个协程分别对上面5个网站进行访问
g_list = []
for i in range(len(urls)):
g = gevent.spawn(func, urls[i], i)
g_list.append(g)
# func(urls[i], i)
gevent.joinall(g_list)
end = time.time()
print(end - start)
6. 使用asyncio模块实现异步协程
该模块适用于Python 3.4及以上版本。asyncio的强大之处在于,当遇到IO操作时,它会自动切换执行其他任务,无需手动干预。
import time
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(1) # 遇到IO耗时操作,自动切换到tasks中的其它任务
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(1) # 遇到IO耗时操作,自动切换到tasks中的其它任务
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end - start)
7. 使用async与await关键字实现异步编程(当前推荐方式)
该语法结构自Python 3.5版本起可用。
import time
import asyncio
async def func1():
print(1)
await asyncio.sleep(1)
print(2)
async def func2():
print(3)
await asyncio.sleep(1)
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end - start)
7.1 事件循环机制
事件循环可视为一个while循环,它周期性运行并执行一系列任务,在满足特定条件时终止循环。
伪代码逻辑如下:
# 伪代码
任务列表 = [ 任务1, 任务2, 任务3,... ]
while True:
可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回
for 就绪任务 in 已准备就绪的任务列表:
执行已就绪的任务
for 已完成的任务 in 已完成的任务列表:
在任务列表中移除 已完成的任务
如果 任务列表 中的任务都已完成,则终止循环
7.2 协程与异步编程基础
协程函数的定义形式为 async def。
协程对象是指调用协程函数后返回的对象实例。
# 定义一个协程函数
async def func():
pass
# 调用协程函数,返回一个协程对象
result = func()
注意:调用协程函数时,函数内部的代码并不会立即执行,而是直接返回一个协程对象。
7.3 基本应用方法
在程序中,若要执行协程函数内部的代码,必须配合使用 事件循环 和 协程对象,例如:
import asyncio
async def func():
print("协程内部代码")
# 调用协程函数,返回一个协程对象。
result = func()
# 方式一
# loop = asyncio.get_event_loop() # 创建一个事件循环
# loop.run_until_complete(result) # 将协程当做任务提交到事件循环的任务列表中,协程执行完成之后终止。
# 方式二
# 本质上方式一是一样的,内部先 创建事件循环 然后执行 run_until_complete,一个简便的写法。
# asyncio.run 函数在 Python 3.7 中加入 asyncio 模块,
asyncio.run(result)
这一过程可简单理解为:将协程作为任务添加到 事件循环 的任务列表中,随后事件循环检测列表中的协程是否已准备就绪(默认为就绪状态)。若就绪,则执行其内部代码。
7.4 await关键字详解
await是仅在协程函数中使用的关键字。当遇到IO操作时,它用于挂起当前协程(任务)。在挂起期间,事件循环可以执行其他协程(任务)。当当前协程的IO处理完成后,程序再次切换回来执行await之后的代码。
await后需跟随可等待对象(包括协程对象、Future对象或Task对象)。
示例1:await与协程对象配合
import asyncio
async def func1():
print("start")
await asyncio.sleep(1)
print("end")
return "func1执行完毕"
async def func2():
print("func2开始执行")
# await关键字后面可以跟可等待对象(协程对象、Future对象、Task对象)
response = await func1()
print(response)
print("func2执行完毕")
asyncio.run(func2())
示例2:在协程函数中可多次使用await关键字
import asyncio
async def func1():
print("start")
await asyncio.sleep(1)
print("end")
return "func1执行完毕"
async def func2():
print("func2开始执行")
# await关键字后面可以跟可等待对象(协程对象、Future对象、Task对象)
response = await func1()
print(response)
response2 = await func1()
print(response2)
print("func2执行完毕")
asyncio.run(func2())
7.5 Task对象管理
Tasks用于并发调度协程。通过asyncio.create_task(协程对象)方式创建Task对象,可使协程加入事件循环等待调度执行。除使用 asyncio.create_task() 函数外,还可使用低层级的 loop.create_task() 或 ensure_future() 函数。官方不建议手动实例化Task对象。
其本质是将协程对象封装为Task对象,并立即将其加入事件循环,同时追踪协程的运行状态。
注意:asyncio.create_task() 函数于Python 3.7版本引入。在Python 3.7之前,可改用低层级的 asyncio.ensure_future() 函数。
示例1:
import asyncio
async def func():
print(1)
await asyncio.sleep(1)
print(2)
return "func的返回值"
async def main():
print(3)
# 创建协程,将协程封装到一个task对象中并立即添加到事件循环列表中,等待事件循环去执行,(默认是就绪状态)
task1 = asyncio.create_task(func())
# 创建协程,将协程封装到一个task对象中并立即添加到事件循环列表中,等待事件循环去执行,(默认是就绪状态)
task2 = asyncio.create_task(func())
# 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
# 此处的await是等待相对应的协程全都执行完毕并获取结果
ret1 = await task1
ret2 = await task2
print(ret1, ret2)
asyncio.run(main())
示例2:此用法较为常见
import asyncio
async def func():
print(1)
await asyncio.sleep(1)
print(2)
return "func的返回值"
async def main():
print(3)
# 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
# 在调用
task_list = [
asyncio.create_task(func()),
asyncio.create_task(func())
]
# 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
# 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
# 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
done, pending = await asyncio.wait(task_list, timeout=None)
print(done)
print(pending)
asyncio.run(main())
示例3:
import asyncio
async def func():
print("执行协程函数内部代码")
# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
response = await asyncio.sleep(2)
print("IO请求结束,结果为:", response)
coroutine_list = [func(), func()]
# 错误:coroutine_list = [ asyncio.create_task(func()), asyncio.create_task(func()) ]
# 此处不能直接 asyncio.create_task,因为将Task立即加入到事件循环的任务列表,
# 但此时事件循环还未创建,所以会报错。
# 使用asyncio.wait将列表封装为一个协程,并调用asyncio.run实现执行两个协程
# asyncio.wait内部会对列表中的每个协程执行ensure_future,封装为Task对象。
done, pending = asyncio.run(asyncio.wait(coroutine_list))
总结:
在程序中,只要看到async和await关键字,即表明内部基于协程实现了异步编程。这种异步编程通过单线程在IO等待期间执行其他任务来实现并发。
场景选择建议: 若为IO密集型且IO请求耗时较长,建议使用协程。 若为IO密集型且IO请求响应较快,建议使用多线程。 若为计算密集型,建议利用多核CPU,采用多进程方案。
以上为Python中asyncio异步编程的详细解析,更多关于Python使用asyncio的资料,请持续关注本站其他相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













