并发:事件循环 & asyncio
2020-08-01 本文已影响0人
dex0423
1. 事件循环机制
1.1. 什么是事件循环
- 事件循环(Event Loop),即通过轮询方法监控事件;
- asyncio 模块的核心,就是事件循环,它可用于执行异步任务、事件回调、执行网络IO操作和运行子进程;
- 事件循环机制分为以下几步骤:
-- 创建一个事件循环;
-- 将异步函数加入事件队列;
-- 执行事件队列, 直到最晚的一个事件被处理完毕后结束;
-- 最后建议用 close() 方法关闭事件循环, 以彻底清理 loop 对象防止误用;
1.2. 事件循环执行机制
- 所有同步任务都在主线程上执行,形成一个 执行栈(execution context stack);
- 主线程之外,还存在一个 任务队列(task queue),只要异步任务有了运行结果,就在”任务队列”之中放置一个事件;
任务队列中的可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
- "任务队列"中的事件,此处指的是 I/O 事件,只要指定过 回调函数,这些事件发生时回调函数就会进入"任务队列",等待主线程读取;
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
- 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;
- "任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取,主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
-
主线程不断重复上面的过程,既是事件循环;
g5pkas2ie6 (1).jpg - 如上图,主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环);
- "任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
- 只要主线程空了,就会去读取”任务队列”,这个过程会循环反复。
1.3. 事件循环的作用
- 事件循环实现了管理事件的所有功能,事件循环在整个程序执行过程中循环运行,并跟踪进程中发生的所有事件:
-- 当主线程空闲时通过调用事件处理程序对它们进行排队和处理;
--当处理程序结束时,控件将传递到被调度的下一个事件。 - Asyncio提供用于管理事件循环的方法如下:
loop = get_event_loop() # 获取当前上下文的事件循环
loop.call_later(time_delay, callback, argument) # 在给定的时间延迟秒之后调用回调函数
loop.call_soon(callback, argument) # 当控制返回到事件循环时调用
loop.time() # 根据事件循环的内部时钟将当前时间返回
asyncio.set_event_loop() # 设置当前上下文的事件循环为循环
asyncio.new_event_loop() # 根据策略的规则创建并返回一个新的事件循环对象
loop.run_forever() # 永远运行直到调用stop()
2. 事件循环 & asyncio
- asyncio 中通过事件循环的方法去激活需要调用的函数;
- 事件循环机制有一个任务调度器 event loop,我们可以把需要执行的 coroutine 打包成 task 加入到 event loop 的调度列表里面(以Handle形式);
- 在 event loop 会检查需要执行那些 task,然后运行这些 task,可能拿到最终结果,也可能执行一半继续 await 别的任务,任务之间互相 wait,通过回调来把任务串联起来;
- 任务可能会依赖别的 I/O 消息,在每一帧,event loop 都会用 selector 处理相应的消息,执行相应的 callback 函数;
- event loop 还可以开线程池处理别的任务,或者,多个线程里执行多个 event loop,他们之间还可以有交互;
- 单个event loop 跑在单个线程有个好处,只要自己不主动 await,就会一直占有主线程,换句话说,同步函数一定没有数据冲突(data racing)。对比多线程方案,如果需要处理数据冲突,就需要加锁了,这在很多情况下会降低程序的性能。所以协程这种设计思路,非常适合有多个用户、但是每个用户之间没有共享数据的场景。如果需要实现并行,多开几个进程就行了;