Python实现协程的四种方式
协程
协程不是计算机提供的,是人为创造的上下文切换技术,也可以被称为微线程。简而言之 其实就是在一个线程中实现代码块相互切换执行。
我们知道正常代码是从上到下依次执行,一个方法或函数操作完毕后才会进入下一个方法或函数执行。例如:
def func1():
print(1)
print(2)
def func2():
print(3)
print(4)
func1()
func2()
此时代码执行逻辑一定是先执行完func1()对象里的语句再执行func2() ,这种称为同步。但是如果我们想在func1()对象中print(1)后切换到func2()该怎么做呢?
可以采用以下几种基于协程的方式:
- greenlet。
- yield 关键字
- asyncio 装饰器(py3.4之后引入)
- async、await关键字(py3.5之后引入)【推荐】
1. greenlet实现协程
# greenlet是第三方模块需先引入
pip3 install greenlet
# -*- coding: utf-8 -*-
# author: micher.yu
# Time:2022/01/08
# simple_desc :
from greenlet import greenlet
def func1():
print(1) # 第二步:输出1
gr2.switch() # 第三步:切换到 func2 函数
print(2) # 第六步:输出2
gr2.switch() # 第七步:切换到func2 函数(如果不切换的话句柄会继续往下执行,也就不会进入func2 输出4)
def func2():
print(3) # 第四步:输出3
gr1.switch() # 第五步:切换到func1 函数
print(4) # 第八步:输出4,func2函数 执行完毕句柄继续往下执行
def func3():
print(5) # 第十步:输出5
gr1 = greenlet(func1) # 此处只是生成greenlet包装的func1对象,代码并不会实际运行
gr2 = greenlet(func2) # 此处生成greenlet包装的func2对象
gr1.switch() # 第一步:此处是正式执行func1()对象
func3() # 第九步:实例化func3
# 所以实际输出会是 1 3 2 4 5
2. yield关键字
不推荐,实际应用场景比较少。
如果对yield关键字还不太熟悉的话可以参考往期这篇文章详解python三大器——迭代器、生成器、装饰器其中生成器部分有详细讲解
def func1():
yield 1
yield from func2() # 这里其实相当于for item in func2(): yield item
yield 2
def func2():
yield 3
yield 4
for item in func1():
print(item)
# 输出结果将会是:1 3 4 2
3. asyncio 模块
在python3.4及之后的版本才可使用,这个框架使用事件循环来编排回调和异步任务。事件循环位于事件循环策略的上下文中。
下图是协程,事件循环和策略之间的相互作用
注意:asyncio
牛逼在于遇到IO阻塞
自动切换!
下面我们使用@asyncio.coroutine
装饰器(py3.10+会移除)定义了两个协程函数。(基于生成器的协程)
import asyncio
@asyncio.coroutine
def func1():
print(1)
# 此处用asyncio.sleep(2)来模拟IO耗时(asyncio.sleep也是一个协程对象,不能用time.sleep()),asyncio定义的协程函数遇到IO操作时会自动切换到事件循环中的其他任务
yield from asyncio.sleep(2)
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2)
print(4)
PS:如果py版本高于3.8依然可以使用asyncio.coroutine
装饰器但是会有告警建议你使用async & await
关键字来定义协程函数,不会影响使用!
协程函数并不能像普通函数一样直接实例化运行,调用协程函数协程并不会开始运行,只是返回一个协程对象。
fun1() # 此处是不会有结果的
可以通过 asyncio.iscoroutine
来验证是否是协程对象
print(asyncio.iscoroutine(func1())) # True
协程对象必须在事件循环中运行,我们可以通过asyncio.get_event_loop
方法来获取当前正在运行的循环实例。如loop
对象,然后把协程对象交给 loop.run_until_complete
,协程对象随后会在 loop
里得到运行。
loop = asyncio.get_event_loop()
loop.run_until_complete(func1())
# 运行结果为:
# 1
# 等待2s
# 2
run_until_complete
是一个阻塞(blocking)调用,直到协程运行结束,它才返回;所以他必须接受的是一个可等待对象
(协程
, 任务
和future对象
)。
可等待对象
:
-
协程对象:协程函数实例化后就是
协程对象
-
future对象:
asyncio.futures.Future对象
用来链接 底层回调式代码 和高层异步/等待式代码,可以简单理解为future对象是可以使程序hang在某个地方等待有结果了之后再继续执行。官方文档- 创建future对象:
loop.create_future()
import asyncio async def main(): # 获取当前事件循环 loop = asyncio.get_running_loop() # 单单只是创建一个future对象 fut = loop.create_future() # future对象因为什么都没做也就没返回值,所以await会一直等待下去程序就会hang住 await fut asyncio.run(main()) print(1)
future对象.set_result()
方法async def func(fut): fut.set_result("finish") async def main(): # 获取当前事件循环 loop = asyncio.get_running_loop() # 单单只是创建一个future对象 fut = loop.create_future() # 创建一个task对象,绑定了func函数并且把我们创建的fut对象传递给了协程对象func;func协程函数内部又对fut对象设置了result await loop.create_task(func(fut)) # 由于设置了fut对象的结果,下面的await就能拿到结果 所以程序就可以继续往下执行了 print(await fut) asyncio.run(main()) print(1) """ 运行结果: finish 1 Process finished with exit code 0 """
- 创建future对象:
-
任务:
Task 对象
是Future对象
的子类,其作用是在运行某个任务的同时可以并发的运行其他任务。
Task 对象
可以使用asyncio.create_task()
函数创建,也可以使用低层级的loop.create_task()
或asnycio.ensure_future()
注意:
asyncio.create_task()
是python3.7之后才有的。python3.7之前可以改用asnycio.ensure_future()
- 取消 Task 对象
cancel()
- Task 任务是否被取消
cancelled()
- Task 对象是否完成
done()
- 返回结果
result()
4.1 Task 对象被完成,则返回结果
4.2 Task 对象被取消,则引发 CancelledError 异常
4.3 Task 对象的结果不可用,则引发 InvalidStateError 异常 - 添加回调,任务完成时触发
add_done_callback(task)
- 所有任务列表
asyncio.all_tasks()
- 返回当前任务
asyncio.current_task()
- 取消 Task 对象
run_until_complete
的参数是一个 future
,但是我们这里传给它的却是协程对象,之所以能这样,是因为它在内部做了检查
要让这个协程对象转成future
的话,可以通过 asyncio.ensure_future
方法(本质其实是创建了个task对象)。
所以,我们可以写得更明显一些:
loop = asyncio.get_event_loop()
loop.run_until_complete(asnycio.ensure_future(func1()))
# 运行结果为:
# 1
# 等待2s
# 2
在有多个协程函数需要同时运行怎么办?
-
我们可以将协程对象包装成
future对象
后再放到一个列表中再通过asyncio.wait
运行。asyncio.wait方法
或await关键字
只能传可等待
对象tasks = [ asyncio.ensure_future(func1()), # 把协程对象包转成一个 future 对象 asyncio.ensure_future(func2()) ] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks)) # 运行结果为: # 1 # 3 # 等待2s # 2 # 4
-
通过
asyncio.gather
可以直接将协程对象放到列表中(必须解包!也就是*[]):loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(*[func1(), func2()])) # 运行结果为: # 1 # 3 # 等待2s # 2 # 4
完整代码为:
import asyncio
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(2) # 此处用asyncio.sleep(2)来模拟IO耗时(asyncio.sleep也是一个协程对象,不能用time.sleep()),自动切换到tasks中的其他任务
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2) # 此处又遇到IO阻塞后,又会自动切换到tasks中其他的任务
print(4)
func1() # 调用协程函数,协程并不会开始运行,只是返回一个协程对象。可以通过 asyncio.iscoroutine 来验证是否是协程对象
print(asyncio.iscoroutine(func1())) # True
tasks = [
asyncio.ensure_future(func1()), # 把协程对象包转成一个 future 对象
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
# 方式一:
loop.run_until_complete(asyncio.wait(tasks))
# 方式二:
loop.run_until_complete(asyncio.gather(*[func1(), func2()]))
同样我们也可以执行Task对象
:
使用 loop
对象的 create_task
函数创建一个 Task
对象,在第一次打印 Task
对象时,状态为 pending
,完成执行函数后的状态为 finished
。
import asyncio
async def do_something():
print("这是一个Task例子....")
# 模拟阻塞1秒
await asyncio.sleep(1)
return "Task任务完成"
# 创建一个事件event_loop
loop = asyncio.get_event_loop()
# 创建一个task
task = loop.create_task(do_something())
# 第一次打印task
print(task)
# 将task加入到event_loop中
loop.run_until_complete(task)
# 再次打印task
print(task)
print(task.result())
""" 运行结果
#1 <Task pending name='Task-1' coro=<do_something() running at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97>>
#2 这是一个Task例子....
#3 <Task finished name='Task-1' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task任务完成'>
#4 Task任务完成
"""
Task
对象的 result()
函数可以获取 do_something()
函数的返回值。
Task 任务回调
import asyncio
async def do_something(task_id):
print(f"这是一个Task例子,当前task_id:{task_id}")
# 模拟阻塞1秒
await asyncio.sleep(1)
return f"Task-id {task_id} 任务完成"
# 任务完成后的回调函数
def callback(task):
# 打印参数
print(task)
# 打印返回的结果
print(task.result())
# 创建一个事件event_loop
loop = asyncio.get_event_loop()
# 创建一个task
tasks = []
for i in range(5):
name = f"task-{i}"
task = loop.create_task(do_something(name), name=name)
task.add_done_callback(callback)
tasks.append(task)
# 将task加入到event_loop中
loop.run_until_complete(asyncio.wait(tasks))
""" 输出为:
这是一个Task例子,当前task_id:task-0
这是一个Task例子,当前task_id:task-1
这是一个Task例子,当前task_id:task-2
这是一个Task例子,当前task_id:task-3
这是一个Task例子,当前task_id:task-4
<Task finished name='task-0' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task-id task-0 任务完成'>
Task-id task-0 任务完成
<Task finished name='task-1' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task-id task-1 任务完成'>
Task-id task-1 任务完成
<Task finished name='task-2' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task-id task-2 任务完成'>
Task-id task-2 任务完成
<Task finished name='task-3' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task-id task-3 任务完成'>
Task-id task-3 任务完成
<Task finished name='task-4' coro=<do_something() done, defined at /Users/mac/Desktop/userspace/TestDemo/test/async_demo.py:97> result='Task-id task-4 任务完成'>
Task-id task-4 任务完成
"""
使用 asyncio.wait()
函数将 Task 任务列表
添加到 event_loop
中,也可以使用 asyncio.gather()
函数。
多个任务执行结束后再回调
import asyncio
import functools
async def do_something(t):
print("暂停" + str(t) + "秒")
await asyncio.sleep(t)
return "暂停了" + str(t) + "秒"
def callback(event_loop, gatheringFuture):
print(gatheringFuture.result())
print("多个Task任务完成后的回调")
loop = asyncio.get_event_loop()
gather = asyncio.gather(do_something(1), do_something(3))
gather.add_done_callback(functools.partial(callback, loop))
loop.run_until_complete(gather)
""" 输出为:
暂停1秒
暂停3秒
['暂停了1秒', '暂停了3秒']
多个Task任务完成后的回调
"""
4. async & await 关键字【推荐🌟】
py3.5及之后版本
本质上和3.4的asyncio
一致,但更强大。
3.5之后yield from
不可以在async
定义的函数内使用,需使用await
。
import asyncio
async def func1():
print(1)
await asyncio.sleep(2) # 遇到IO自动切换任务
print(2)
async def func2():
print(3)
await asyncio.sleep(2) # 此处又遇到IO阻塞后,又会自动切换到tasks中其他的任务
print(4)
tasks = [
asyncio.ensure_future(func1()), # 把协程对象包转成一个 future 对象
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
# 执行单个协程函数
loop.run_until_complete(func1()) # 由于func1中使用了await关键字所以此处等同于asyncio.wait
""" 输出结果为:
1
等待2s
2
"""
# 执行多个协程函数
loop.run_until_complete(asyncio.wait(tasks))
""" 输出结果为:
1
3
等待2s
2
4
"""
注:python3.7之后可以不需要自己获取loop对象,可以直接调用asyncio.run方法
内部已经帮我们获取了loop对象和调用loop.run_until_complete
直接使用(但是不支持同时运行多个协程对象):
asyncio.run(func1())
async & await 关键字
简化代码的同时并且兼容基于生成器的老式协程
@asyncio.coroutine
def old_style_coroutine():
yield from asyncio.sleep(1)
async def main():
await old_style_coroutine()
一个协程函数中可以使用多次await关键字
import asyncio
async def func():
print("start")
await asyncio.sleep(5)
print("end")
return "finish"
async def main():
print("执行main方法")
resp1 = await func()
print(f"第一次返回值:{resp1}")
resp2 = await func()
print(f"第二次返回值:{resp2}")
asyncio.run(main())
"""输出为:
"""
同样我们也可以在一个协程函数中获取多个其他协程对象的返回值:
import asyncio
async def func():
print(1)
await asyncio.sleep(2)
print(2)
return f"func was done"
async def main():
print("main开始")
# 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
# 在调用
task_list = [
asyncio.create_task(func(), name="n1"),
asyncio.create_task(func(), name="n2")
]
print("main结束")
# 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
# 此处的await是等待所有协程执行完毕,并返回两个集合 done、pending。done存放已完成的task对象,pending存放未完成的task对象
# 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
done, pending = await asyncio.wait(task_list, timeout=None)
print(done, pending)
print(list(done)[0].result())
asyncio.run(main())
"""输出为:
main开始
main结束
1
1
2
2
{<Task finished name='n2' coro=<func() done, defined at /Users/mac/Desktop/userspace/TestDemo/test.py:33> result='func was done'>, <Task finished name='n1' coro=<func() done, defined at /Users/mac/Desktop/userspace/TestDemo/test.py:33> result='func was done'>} set()
func was done
"""
有同学可能就要问了:上面好像都是协程调用协程函数,那我普通函数能不能和协程函数互相调用?
5.协程函数与普通函数搭配使用
普通函数调用协程函数:
async def func3():
print('i`m func3')
await asyncio.sleep(2)
print('func3 finished')
def func4():
print('i`m func4')
asyncio.run(func3())
print('func4 finished')
func4()
"""输出结果:
i`m func4
i`m func3
等待2s
func3 finished
func4 finished
"""
协程函数调用普通函数
def func5():
print('i`m func5')
time.sleep(2)
print('func5 finished')
async def func6():
print('i`m func6')
func5()
print('func6 finished')
asyncio.run(func6())
print('all finish')
"""
i`m func6
i`m func5
等待2s
func5 finished
func6 finished
all finish
"""
有人就要问了这跟同步
没区别啊,搞得花里胡哨的有啥用;我一个普通函数一样能实现这种效果。
那大家可以思考下这个问题,假设你的接口处理完必要业务后还有部分后置操作成功与否并不影响你的业务逻辑(比如点赞场景、记录操作日志这种无关紧要的后置更新场景等);假设插入或更新db非常耗时 ,你为了提高性能会怎么处理呢?
6.concurrent.futures.Future对象
使用线程池、进程池实现异步操作时用到的future对象。它跟asyncio.futures.Future
没有关系,但是也是帮助程序hang住直到拿到结果。
进程池和线程池
线程池:用于伪并发执行,GIL锁的原因;但是其实遇到IO就切换所以线程启动间隔时间其实很短
from concurrent.futures.thread import ThreadPoolExecutor
def func(value):
print("start")
time.sleep(1)
print(value)
# 指定同时可用的线程数量
thread_pool = ThreadPoolExecutor(max_workers=5)
for i in range(10):
# submit(func,*args,**kwargs)方法向线程池提交任务
fut = thread_pool.submit(func, i)
# 返回值为future对象
print(type(fut)) # <class 'concurrent.futures._base.Future'>
执行逻辑为:
先提交一个任务到线程池中 》 打印start 》遇到IO切换任务 》 打印 future 对象类型 》提交第二个任务到线程池中 》打印 start 》遇到IO切换任务 》打印 future 对象类型 》...打印 start 》 遇到IO切换任务 》打印 future 对象类型..(如果IO处理完了会在上述步骤中穿插输出 0/1/2/3/4)
等前5个有执行完毕了再提交后面的任务,同一时刻线程池中只维护5个活跃线程。处理流程与前5个一致
所以输出为:
start
<class 'concurrent.futures._base.Future'>
start
<class 'concurrent.futures._base.Future'>
start
<class 'concurrent.futures._base.Future'>
start
<class 'concurrent.futures._base.Future'>
start
<class 'concurrent.futures._base.Future'>
<class 'concurrent.futures._base.Future'>
<class 'concurrent.futures._base.Future'>
<class 'concurrent.futures._base.Future'>
<class 'concurrent.futures._base.Future'>
<class 'concurrent.futures._base.Future'>
1
start
2
start
3
start0
start
4
start
5
678
9
进程池:创建多个进程并发执行
from concurrent.futures.process import ProcessPoolExecutor
def func(value):
print("start")
time.sleep(1)
print(value)
process_pool = ProcessPoolExecutor(max_workers=5)
if __name__ == '__main__':
for i in range(10):
# 返回值为concurrent.futures._base.Future对象
fut = process_pool.submit(func, i)
执行逻辑为:
启动5个进程分别执行任务》打印5次start 》IO阻塞一秒 》打印01234
等前5个有执行完毕了再创建新的进程提交后面的任务,同一时刻进程池中只维护5个活跃进程。处理流程与前5个一致
输出为:
start
start
start
start
start
0
start
1
2
start
start
3
start
4
start
5
6
7
8
9
回到上面的问题在不采用异步的方式时:
程序执行需要5s多这显然不是我们想要的,采用异步我们可以通过以下几种方式解决:
-
loop.run_in_executor(None,func)
def insert_db(second=random.randint(1, 5)): print("开始执行插入语句") time.sleep(second) print(f"数据插入完成,耗时:{second} s") async def main(): print("执行main") loop = asyncio.get_running_loop() # 第一步:第一个参数传None则内部会先调用ThreadPoolExecutor 的submit方法去线程池中申请一个线程去执行insert_db函数,并返回一个concurrent.futures.Future对象 # 第二步:调用asyncio.wrap_future将concurrent.futures.Future对象包装为asyncio.Future对象 # asyncio.Future对象才能使用await语法 loop.run_in_executor(None, insert_db) # 不等待函数执行完毕,异步 # await loop.run_in_executor(None, insert_db) # 等待执行完毕,就是同步 print("main执行结束") before = time.time() print(f"执行前时间:{before}") asyncio.run(main()) after = time.time() print("程序结束") print(f"执行后时间:{after},总耗时:{after - before}")
执行结果为:
-
ThreadPoolExecutor.submit(func,*args)
:替换成loop.run_in_executor(ThreadPoolExecutor(), insert_db)
即可 -
ProcessPoolExecutor.submit(func,*args)
:
7.普通函数使用多线程或多进程
多线程:
多进程:上文ThreadPoolExecutor
替换成ProcessPoolExecutor
即可
我们也可以结合装饰器将一个普通函数包装成concurrent.futures.Future对象从而进一步简化,如:
from functools import wraps
def wrap_to_async(thread_num=2):
def wrapper(func):
@wraps(func)
def inner_wrapper(*args):
return ThreadPoolExecutor(thread_num).submit(func, *args)
return inner_wrapper
return wrapper
async def func1():
print('i`m func1')
func2()
print('func1 finished')
@wrap_to_async(2)
def func2():
print('i`m func2\n')
time.sleep(2)
print('func2 finished')
asyncio.run(func1())
print("all finished")
后续使用时只需在需异步操作的普通函数上加上装饰器即可实现协程。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具