前面我们一直使用到关于事件循环,我们也深知事件循环是asyncio库的核心机制,它是实现我们的实现代码以非阻塞的方式执行,从而可以同时处理多个异步操作的关键,且需要注意,通常我们的事件循环都是在一个单线程内运行的。
可能我们有疑问了,为什么我们单线程内也可以实现并发任务?不应该所有的任务都是串行的吗?所以这也是我们使用异步的关键点之一。
在Python的asyncio库中,事尽管我们在单线程的执行所有的异步操作和任务,但是基于协程和事件循环的机制,就非常巧妙的实现了非阻塞的操作,允许在单个线程中并行处理多个任务。asyncio库中实现并发机制是通过协作式多任务和事件驱动编程来实现的,而不是通过线程之间的并行执行。
在事件循环中,我们的‘任务’(包括协程和任务等)是按照它们被添加到事件循环的顺序执行,但是我们的每个任务在执行过程期间如果遇到IO阻塞,如果一个任务在进行I/O操作,事件循环可以切换到执行其他任务,直到I/O操作完成。当I/O操作返回结果时,事件循环会处理这个事件,并恢复执行之前被挂起的任务。这样就可以更高效的实现任务的并发处理。
因此我们的事件循环可以同时管理多个协程和任务,而不需要创建多个线程或进程来进行处理。
首先我们需要了解一下,事件循环的主要职责包括:
在前面其实我们很少直接和我们的事件循环打交道,多数的情况下,我们都有对应封装好的函数可以直接调用,常见的有:
loop.create_task() 用于将协程任务注册到指定的事件循环中。
asyncio.get_event_loop():获取当前事件循环。
asyncio.run():启动事件循环并运行一个协程或函数。这个函数会创建一个事件循环,运行指定的main协程或函数,并在完成后关闭事件循环。通常不需要手动获取事件循环,因为asyncio.run()函数会为你创建并管理事件循环。
loop.create_task(coro):创建一个任务并将其添加到事件循环中。这个方法会返回一个任务对象,可以用来跟踪协程的执行。
loop.run_until_complete(future):运行事件循环,直到指定的异步事件(通常是一个任务或一个Future对象)完成。
loop.run_in_executor(executor, func, *args, **kwargs):在指定的线程池执行器中运行一个函数。这个方法允许你在事件循环外部执行阻塞操作,而不会阻塞事件循环的执行。loop.run_in_executor 返回一个 Future 对象,我们可以使用 await 关键字来等待该 Future 对象的完成并获取它的返回值。由于该函数是异步的,因此它不会阻塞当前线程的执行。
loop.call_soon(func, *args, **kwargs):在事件循环的下一个迭代中调用一个函数。
loop.call_later(delay, func, *args, **kwargs):在指定的延迟后调用一个函数。
loop.call_at(when, func, *args, **kwargs):在指定的时间点调用一个函数。
loop.stop()、loop.close():关闭事件循环。
loop.run_forever()一般会结合loop.stop()一起进行使用。
下面我们会尝试去理解和应用上面一些常见的方法。
我们知道通常我们的运行一个协程使用的是上面的方法才可以。似乎我们的整个过程都没手动的进行过相关进行手动的创建我们的事件循环对象,原因是:asyncio.run()内部以及自动的帮我们创建好了。也就是说当我们使用asyncio.run()一个协程或任务的时候:
asyncio.run() 会自己创建一个 EventLoop 实例,它开始负责协调异步任务、定时器、协程等的执行。当我们的 asyncio.run() 被调用时,它会尝试获取当前线程的 EventLoop,如果没有则创建一个新的。
设置事件循环:在获取或创建 EventLoop 后,我们的asyncio.run()内部会把这个EventLoop事件循环设置为当前线程的事件循环。此时此刻,也意味着在这个线程中所有的 async 函数都会被这个事件循环管理。
执行主协程:当钓鱼asyncio.run() 时,系统会将会执行一个被称为“主协程”的 async 函数。主协程是异步应用的入口点,通常包含应用程序的主要逻辑。通过 asyncio.run() 传入的协程函数在这个阶段被执行。
等待主协程完成:asyncio.run() 会等待主协程执行完成。在主协程执行完毕后,事件循环会继续运行,直到没有更多的任务需要执行。
关闭事件循环:当主协程执行完毕后,asyncio.run() 会关闭事件循环,释放资源。这通常意味着所有的事件循环内部资源,包括如网络连接和定时器等等,都会被清理。
返回结果:asyncio.run() 在关闭事件循环后会返回主协程的结果。如果主协程是一个 Task 对象,这个结果将是 Task 的结果。如果没有设置返回值,则返回 None。
如下示例:
import asyncio
async def ss():
return "我是小钟同学"
print(asyncio.run(ss()))
输出结果为:
我是小钟同学
说明了我们的主协程返回值就是协程最终的返回值。还是需要再强调一次:为了保证 asyncio 的线程安全,asyncio.run() 在执行过程中会确保只有一个线程能够访问事件循环,这也意味着在多线程环境中 asyncio.run() 只能调用一次。如果在同一个线程中多次调用 asyncio.run(),后续的调用将会抛出 RuntimeError。如下示例代码:
import asyncio
async def ss():
return "我是小钟同学"
async def main():
print("main")
print(asyncio.run(ss()))
print("main end")
print(asyncio.run(main()))
运行会会得到如下错误结果:
Traceback (most recent call last):
File "D:\code_loacl\mm_ring_v2\ss.py", line 11, in <module>
print(asyncio.run(main()))
^^^^^^^^^^^^^^^^^^^
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 190, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Program Files\Python311\Lib\asyncio\base_events.py", line 653, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "D:\code_loacl\mm_ring_v2\ss.py", line 8, in main
print(asyncio.run(ss()))
^^^^^^^^^^^^^^^^^
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 186, in run
raise RuntimeError(
RuntimeError: asyncio.run() cannot be called from a running event loop
sys:1: RuntimeWarning: coroutine 'ss' was never awaited
在上面我们都是直接的使用封装好的创建事件对象,当然我们也可以手动的创建,如下示例代码所示:
import asyncio
async def main():
print("我是谁")
await asyncio.sleep(1)
print("我是同学")
# 创建事件循环
loop = asyncio.get_event_loop()
# 运行事件循环
try:
loop.run_until_complete(main())
except KeyboardInterrupt:
print("程序被终止")
finally:
loop.close()
需注意的是,如果我们手动的创建事件循环的话,则需要注意:
示例1:在事件循环处进行在协程内部使用try/except
import asyncio
async def my_coroutine():
# 异步任务的具体实现
await asyncio.sleep(1)
raise Exception("协程内部出现异常")
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(my_coroutine())
except Exception as e:
print("捕获到异常:", e)
loop.stop()
finally:
loop.close()
示例2:在协程内部使用try/except
import asyncio
async def my_coroutine():
try:
# 协程任务的具体实现
await asyncio.sleep(1)
raise ValueError("协程内部出现异常")
except ValueError as e:
print("捕获到异常:", e)
loop = asyncio.get_event_loop()
loop.run_until_complete(my_coroutine())
示例3:使用asyncio.gather()来集合多个协程的执行,并使用return_exceptions=True参数来捕获所有异常.
import asyncio
async def my_coroutine():
# 协程任务的具体实现
await asyncio.sleep(1)
raise ValueError("协程内部出现异常")
async def main():
coroutines = [my_coroutine(), my_coroutine()]
try:
results = await asyncio.gather(*coroutines, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
print("捕获到异常:", result)
except Exception as e:
print("捕获到异常:", e)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.create_task(coro)主要是一个用于将协程任务注册到指定的事件循环中的方法。loop 表示要注册任务的事件循环对象,coro 则是一个协程函数或协程对象。该方法会返回一个 Task 对象,表示被创建的任务。任务会在事件循环中被调度执行。
主要应用:
注册和调度协程任务:将我们的协程或任务注册到指定的事件循环中。被注册的任务将会被事件循环调度执行,从而实现并发和异步相关的操作。
并发执行多个任务:我们可以在同一个事件循环中注册多个协程任务,并发地执行它们。
控制任务的执行顺序和优先级:通过适当地安排和调度任务的注册顺序,可以控制任务的执行顺序和优先级。
处理任务的结果和异常:通过获取 Task 对象并监视其状态,可以处理任务的执行结果和异常。
并发执行多任务的应用:
import asyncio
# 在第一个事件循环中运行任务
async def task1():
while True:
print("Task 1")
await asyncio.sleep(1)
# 在第二个事件循环中运行任务
async def task2():
while True:
print("Task 2")
await asyncio.sleep(2)
# 创建并运行多个独立的事件循环
async def main():
# 创建第一个事件循环并注册任务
loop1 = asyncio.get_running_loop()
loop1.create_task(task1())
loop1.create_task(task2())
# 运行事件循环
await asyncio.gather(*asyncio.all_tasks())
# 启动程序
asyncio.run(main())
上面程序的运行结果如下:
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 2 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 2 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 2 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1297158880336
上面的程序我们的是使用的是同一事件循环,加入我们需要不同的事件循环呢?也就是不同的线程使用不同的事件循环?如下示例代码所示:
import asyncio
import threading
# 创建第一个事件循环
loop1 = asyncio.new_event_loop()
# 创建第二个事件循环
loop2 = asyncio.new_event_loop()
# 在第一个事件循环中运行任务
async def task1():
while True:
print("Task 1",asyncio.get_running_loop(),id(asyncio.get_running_loop()))
await asyncio.sleep(1)
loop1.create_task(task1())
# 在第二个事件循环中运行任务
async def task2():
while True:
print("Task 2",asyncio.get_running_loop(),id(asyncio.get_running_loop()))
await asyncio.sleep(2)
loop2.create_task(task2())
# 启动两个事件循环
thread1 = threading.Thread(target=loop1.run_forever)
thread1.start()
thread2 = threading.Thread(target=loop2.run_forever)
thread2.start()
输出结果如下:
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1682941131408
Task 2 <ProactorEventLoop running=True closed=False debug=False> 1682941165840
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1682941131408
Task 2 <ProactorEventLoop running=True closed=False debug=False> 1682941165840
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1682941131408
Task 1 <ProactorEventLoop running=True closed=False debug=False> 1682941131408
从输出结果可以得知我们的事件循环的对象不同了!!!
ProactorEventLoop 是 Python 标准库 asyncio 模块提供的一种事件循环实现方式,它是基于 Windows IOCP (I/O Completion Ports) 的事件循环。后续会针对这个事件循环展开说明。
在前面可知,我们的loop.run_in_executor 可以在指定的线程池执行器中运行一个函数。这个方法允许你在事件循环外部执行阻塞操作,而不会阻塞事件循环的执行。也就是即使我们的在单线程内跑阻塞的任务,也可以把阻塞的任务进行异步化处理。如下示例代码所示:
import asyncio
import time
import requests
def blocking_sync_func(arg):
# 阻塞的同步函数,模拟耗时操作
resylt = requests.get('https://www.baidu.com')
return resylt.status_code
async def main():
loop = asyncio.get_event_loop()
start_time = time.time()
print("开始时间",start_time)
tasks = [loop.run_in_executor(None, blocking_sync_func, i) for i in range(100)]
results = await asyncio.gather(*tasks)
end_time = time.time()
print("结束时间",end_time)
print(f"总共用时:{end_time-start_time:.2f}s")
print(f"结果列表:{results}")
asyncio.run(main())
在上面的代码中,我们定义了一个阻塞的同步函数blocking_sync_func(),它里面并且使用requests进行阻塞任务的执行,并返回请求处理结果状态。在main()协程中,我们使用loop.run_in_executor()将这个同步函数转为异步操作,并把他们都放在后台线程中执行,且我们循环调用blocking_sync_func()10次。
前面我们知道loop.run_in_executor()返回一个 Future 对象,我们可以使用 await 关键字来等待该 Future 对象的完成并获取它的返回值,并使用asyncio.gather()等待所有异步任务完成。由于所有函数都是异步的,因此它不会阻塞当前线程的执行。由于使用了loop.run_in_executor(),所以我们可以同时运行多个阻塞的同步函数,而不会阻塞事件循环。
上面示例输出的结果如下:
开始时间 1705136380.8293176
结束时间 1705136386.6214948
总共用时:5.79s
结果列表:[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
这个方法用于安排一个回调函数在事件循环中【尽快被执行】。它接受一个可调用对象作为参数,并将其放入事件循环的待执行队列中。当事件循环空闲时,会立即执行这个回调函数。使用 loop.call_soon() 可以实现一些简单的异步操作
但是需要注意回调函数的执行时间,避免阻塞事件循环。如果回调函数需要执行一些耗时的操作,可以考虑使用 loop.run_in_executor() 将这些操作放到线程池中执行,避免阻塞事件循环。
如下一个示例,:
import asyncio
import time
def sync_func():
print("开始执行同步函数")
time.sleep(5)
print("同步函数执行完毕")
async def main():
loop = asyncio.get_event_loop()
# 使用 loop.call_soon() 安排同步函数的执行
loop.call_soon(loop.run_in_executor, None, sync_func)
# 运行事件循环直到完成指定的任务
await asyncio.sleep(1)
loop.stop()
# 运行主协程
asyncio.run(main())
运行代码会会提示如下的错误:
开始执行同步函数
Traceback (most recent call last):
File "D:\code_loacl\mm_ring_v2\ss222333333344444.py", line 21, in <module>
asyncio.run(main())
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 189, in run
with Runner(debug=debug) as runner:
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 63, in __exit__
self.close()
File "D:\Program Files\Python311\Lib\asyncio\runners.py", line 73, in close
loop.run_until_complete(loop.shutdown_default_executor())
File "D:\Program Files\Python311\Lib\asyncio\base_events.py", line 651, in run_until_complete
raise RuntimeError('Event loop stopped before Future completed.')
RuntimeError: Event loop stopped before Future completed.
同步函数执行完毕
上面可以看出我们的逻辑处理,由于在事件循环结束前,也就是我们的await asyncio.sleep(1)之后,就停止了事件循环,但是由于我们的安排的任务sync_func还没有被完成或取消,因此抛出了 RuntimeError。
需要注意,我们的loop.call_soon() 可以安排执行同步的任务也可以安排异步的任务,如下的示例:
import asyncio
import time
def callback_with_args(arg1, arg2):
time.sleep(1)
print(f"同步回答的参数: {arg1}, {arg2}")
async def main():
loop = asyncio.get_event_loop()
# 使用 loop.call_soon() 安排带有参数的回调函数的执行
loop.call_soon(callback_with_args, "1", "2")
# 定义一个异步任务
async def my_task():
await asyncio.sleep(2)
print("异步任务完成")
# 使用 loop.call_soon() 安排异步任务的执行
loop.call_soon(asyncio.create_task, my_task())
# 运行事件循环直到完成指定的任务
await asyncio.sleep(3)
loop.stop()
# 运行主协程
asyncio.run(main())
附加说明:loop.call_soon_threadsafe(callback, *args, context=None)是call_soon()函数的线程安全版本,计划回调函数必须在另一个线程中使用。
loop.call_soon_threadsafe() 方法通常在以下情况下使用:
import asyncio
import threading
def blocking_task():
"""
阻塞任务,会等待2秒钟然后返回"后台任务完成"
"""
import time
time.sleep(2)
return "后台任务完成"
def run_blocking_task(loop, future):
"""
在主线程之外创建一个新线程来执行阻塞任务
"""
result = blocking_task()
loop.call_soon_threadsafe(future.set_result, result)
def main():
"""
主函数
"""
# 创建事件循环
loop = asyncio.get_event_loop()
# 创建一个Future对象
future = asyncio.Future()
# 在主线程之外创建一个新线程来执行阻塞任务
thread = threading.Thread(target=run_blocking_task, args=(loop, future))
thread.start()
# 在主线程中执行异步任务
async def run_async_task():
awaitable = asyncio.wrap_future(future)
await asyncio.ensure_future(awaitable)
loop.run_until_complete(run_async_task())
# 获取阻塞任务的结果
loop.run_until_complete(future)
blocking_result = future.result()
print('后台任务的结果',blocking_result)
# 关闭事件循环
loop.close()
if __name__ == "__main__":
main()
在上面的代码中我们
在main()函数中我们整个的流程处理如下:
loop.call_later() 用于在指定时间后调度一个回调函数的执行。它接受两个参数:delay 和 callback。
示例代码如下:
import asyncio
import functools
async def light_candles():
print("light_candles...开始")
await asyncio.sleep(3) # 模拟异步操作
print("light_candles!!!结束")
def callback():
print('Callback function called')
async def main():
loop = asyncio.get_running_loop()
loop.call_later(2, callback) # 延迟 2 秒后调用 callback 函数
loop.call_later(3, asyncio.create_task, light_candles())
print('为了看到延迟后执行的效果!!!!')
await asyncio.sleep(6)
print('延迟效果结束')
asyncio.run(main())
输出的结果如下:
为了看到延迟后执行的效果!!!!
Callback function called
light_candles...开始
延迟效果结束
light_candles!!!结束
修改我们的light_candles()里面的休眠时间为4或更长的时候,则会输出:
为了看到延迟后执行的效果!!!!
Callback function called
light_candles...开始
延迟效果结束
由此可见,loop.call_later() 并不是一个精确的定时器,它只是在指定的时间后调度回调函数的执行。实际的延迟时间可能会受到事件循环的负载和其他因素的影响,被调度的任务如果太久,有可能也不会正常被执行!
结合FastAPI我们可以使用它做类似异步的后台的任务的功能哟@@@@@如下示例代码所示:
import asyncio
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/delayed_task")
async def delayed_task():
loop = asyncio.get_running_loop()
async def task():
await asyncio.sleep(1)
print('延迟的任务异步的执行了!!!!!')
return {"message": "Delayed task executed"}
def callback():
asyncio.create_task(task())
loop.call_later(1, callback)
return {"message": f"延迟任务的执行演示"}
上面我们请求接口会立即完成我们的接口的请求回应,但是大约两秒后就会输出的我们的结果‘延迟的任务异步的执行了!!!!’!
loop.call_at() 用于在指定的时间点调度执行某个回调函数。
示例应用:
import asyncio
import time
import uvicorn
from fastapi import FastAPI
app = FastAPI()
def async_task():
time.sleep(2)
print("异步定时任务执行结束")
def schedule_task():
loop = asyncio.get_event_loop()
when = loop.time() + 1
loop.call_at(when, async_task)
@app.get("/schedule")
async def schedule():
schedule_task()
return {"message": "Task scheduled"}
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8050)
注意上面的代码,我在延迟函数的调用是阻塞了!!!!time.sleep(2),你可以自己体验是什么感觉!!!
asyncio.new_event_loop()用于创建一个新的事件循环(event loop)对象,并返回该对象。通常会结合我们的 set_event_loop()来一起使用。如下示例:
import asyncio
async def my_task():
print("Running my task")
# 创建事件循环对象
loop = asyncio.get_event_loop()
# 设置默认事件循环
asyncio.set_event_loop(loop)
# 注册任务到默认事件循环并运行
loop.run_until_complete(my_task())
文笔有限,如有笔误或错误!欢迎批评指正!感谢各位大佬!有什么问题也可以随时交流!
END
简书:https://www.jianshu.com/u/d6960089b087
掘金:https://juejin.cn/user/2963939079225608
公众号:微信搜【程序员小钟同学】
新开QQ群号,欢迎随时加群交流,相互学习。QQ群号:247491107
小钟同学 | 文 【欢迎一起学习交流】| QQ:308711822
原文作者:程序员小钟同学;如有侵权,请联系管理员删除。