asyncio 真的是好复杂,官方文档也只是在说明一些概念,看了也不懂。
大致看了一些 asyncio 的源码,也看了一些文章,决定自己试着实现一个 asyncio 。
- 事件循环
从 asyncio 的 api 来看,它实现了一个事件循环。事件循环的基本思想可以用下面这段伪代码来表示。
class EventLoop:
def register(self, event, callback):
self._callbacks[event] = callback
def start(self):
while True:
# 等待事件发生
events = self.wait()
for event in events:
self._callbacks[event]()
loop = EventLoop()
loop.register(event, callback)
loop.start()
可以分为这么几部分:注册事件回调、事件循环;事件循环包括事件等待、执行事件回调。
我们要记住这个核心逻辑,事件循环的实现便是在这上面添砖加瓦,暴露出不同的api给开发者使用。
- hello world
我们先来看 asyncio 的 hello world 例子,今天这边文章便是要实现这个例子
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
# Python 3.7+
asyncio.run(main())
我把自己的包命名为 ayqio
,也就是说我们要实现 ayqio.sleep
和 ayqio.run
。
这个例子所等待的事件是个时间事件,我们先实现时间事件相关的逻辑。
其中有很多 api 我会狠狠地复刻 asyncio
的(不用自己起名字真爽( ̄_, ̄ ))。
先实现一个处理时间事件的事件循环。
逻辑是这样的:我们先注册执行时间和回调,在事件循环中遍历时间事件,获得最小等待时间,然后 time.sleep
到指定时间即可。这里我们使用最小堆排序时间事件。
import heapq
import time
import datetime
import typing
def log(msg):
print(f"[{datetime.datetime.now()}]{msg}")
class TimerHandle(object):
"""定时处理器"""
def __init__(self, when: float, callback, *args):
self.when = when
self._callback = callback
self._args = args
def run(self):
"""执行回调"""
log(f"run callback {self._callback}, args: {self._args}")
if self._callback is not None:
self._callback(*self._args)
class EventLoop(object):
_instance = None
def __init__(self):
# 调度的定时处理器
self._scheduled: typing.List[TimerHandle] = []
# 就绪的定时处理器
self._ready: typing.List[TimerHandle] = []
self._stopping = False
def run_forever(self):
while True:
self._run_once()
if self._stopping:
break
def _run_once(self):
"""进行一次事件循环"""
# 计算下次事件循环等待事件
timeout: typing.Optional[float] = None
if self._ready or self._stopping:
timeout = 0
elif self._scheduled:
when = self._scheduled[0].when
# 0 <= timeout
timeout = max(0, when - self.time())
if timeout:
time.sleep(timeout)
# 寻找就绪的定时处理器
end_time = self.time()
while self._scheduled:
handle = self._scheduled[0]
if handle.when > end_time:
break
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
self._ready.append(handle)
# 执行定时处理器
# 统一一个地方执行定时处理器,循环过程中增加的就绪处理器放到下次循环进行
ntodo = len(self._ready)
for i in range(ntodo):
handle = self._ready.pop(0)
handle.run()
def time(self):
return time.monotonic()
def call_later(self, delay: typing.Union[int, float], callback, *args):
"""在 delay 秒后执行回调"""
return self.call_at(self.time() + delay, callback, *args)
def call_at(self, when: float, callback, *args):
"""在 when 时间执行回调"""
timer = TimerHandle(when, callback, *args)
# 定时事件回调使用最小堆排序
heapq.heappush(self._scheduled, timer)
return timer
def call_soon(self, callback, *args):
"""尽快执行回调"""
return self.call_at(self.time(), callback, *args)
def stop(self):
self._stopping = True
# 全局的事件循环
_event_loop = EventLoop()
def get_event_loop():
return _event_loop
def run(coroutine):
loop = get_event_loop()
loop._run_once()
接下来,我们在同一个目录下新建 helloworld.py
文件
import ayqio
def world():
print('... World!')
def main():
print('Hello ...')
loop = ayqio.get_event_loop()
loop.call_later(1, world)
ayqio.run(main())
执行 helloworld.py
文件后,我们可以看到 print('... World!')
在等待了1秒之后才执行。
总之,我们实现了一个可以处理时间事件的事件循环。后面我们要改造成跟 asyncio
一样的 api 。
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
# Python 3.7+
asyncio.run(main())
我们要来拆解这个例子的逻辑,将它与我们上面说的事件循环基本思想结合起来。
事件循环先是要注册事件回调,而在上面的代码里,便是 await asyncio.sleep(1)
注册了一个时间事件,时间事件触发后,要执行 print('... World!')
,也就是要驱动协程往下走。流程如下
注册时间事件回调 --> 等待 1s(sleep) --> 调用回调 --> 驱动协程 (main)
写成代码就是
coroutine = main()
obj = coroutine.send(None) # obj 由 sleep 返回
obj.add_coroutine_callback(coroutine)
一方面协程需要激活,执行到 await
处,另一方面协程执行完成后会抛出异常 StopIteration
,因此我们把协程封装起来。上面代码中的 obj
我们参照 python
原有的 Future
。完整代码如下
# Future 状态
_PENDING = 'PENDING'
_CANCELLED = 'CANCELLED'
_FINISHED = 'FINISHED'
class Future(object):
def __init__(self, loop):
self._result = None
self._exception = None
self._state = _PENDING
self._callbacks = []
self._loop = loop
def result(self):
if self._state != _FINISHED:
raise ValueError('Result is not ready.')
if self._exception is not None:
raise self._exception
return self._result
def set_result(self, result):
self._result = result
self._state = _FINISHED
self.__schedule_callbacks()
def set_exception(self, exception):
self._exception = exception
self._state = _FINISHED
self.__schedule_callbacks()
def add_done_callback(self, callback):
self._callbacks.append(callback)
def __schedule_callbacks(self):
"""把回调交给事件循环执行"""
callbacks = self._callbacks[:]
if not callbacks:
return
# 清空回调
self._callbacks[:] = []
for callback in callbacks:
self._loop.call_soon(callback, self)
def __await__(self):
"""成为 awiatable 对象,可以使用 await """
# 返回 future ,注册回调驱动协程
yield self
# 返回结果
return self._result
class Task(object):
"""驱动协程执行"""
def __init__(self, coroutine, loop):
self._loop = loop
self._coroutine = coroutine
# 激活协程
self._loop.call_soon(self.__step)
def __step(self, exc=None):
try:
if exc is None:
result = self._coroutine.send(None)
else:
result = self._coroutine.throw(exc)
except StopIteration as e:
log(f"coroutine {self._coroutine} finish, return {e.value}")
except Exception as e:
log(f"coroutine {self._coroutine} step catch e: {e}")
else:
result.add_done_callback(self.__wakeup)
def __wakeup(self, future):
try:
future.result()
except Exception as e:
self.__step(e)
else:
self.__step()
下面我们来实现 sleep
逻辑,它要注册时间事件回调,返回一个 Future
对象。同时我们将创建 Future
和 Task
封装到 EventLoop
中。
class EventLoop(object):
def create_future(self):
return Future(self)
def create_task(self, coroutine):
return Task(coroutine, self)
# 标准库 asyncio 里面的 sleep 是个协程,这里实现的只是一个普通的函数
# return 的 Future 就是一个协程,实现了 `__await__` 方法
def sleep(seconds):
loop = get_event_loop()
f = loop.create_future()
loop.call_later(seconds, f.set_result, None)
return f
事件循环还需要提供方法,用来驱动协程直至协程结束。直接的思路便是注册回调,在协程执行完成后执行回调停止事件循环,我们直接用 Task
继承 Future
来实现。
完整的流程如下
- run 函数根据传入的协程 main() 构建 Task ;
- Task 的实例化过程
__init__
注册回调到事件循环,这个回调负责驱动协程,执行到 sleep 处的 await ; - 开始事件循环,执行第 2 步中的回调,即 Task.__step 方法;
- Task.__step 方法驱动协程,执行到 await sleep 处;
- sleep 构建 Future ,注册时间事件
Future.set_result
到事件循环,返回构建好的Future
; - 控制流回到
Task.__step
方法, result 接收 await sleep 返回的 Future ,同时将下一次协程的执行 (Task.__wakeup
) 注册到 Future 的回调; - 控制流回到事件循环,开始下一次循环;
- 1s 过后,事件循环执行时间事件回调
Future.set_result
,Future
开始执行它的回调,即第 6 步中注册的Task.__wakeup
; -
Task.__wakeup
读取执行结果,驱动协程执行下一步,Future.__await__
方法被执行,返回Future
的result
,回到main
继续向下执行,此时main
协程执行完毕,抛出StopIteration
,在Task.__step
方法中捕获,调用Task.set_result
方法,执行Task
本身的回调,这个回调会停止事件循环。
完整代码见
v1.0 · yeqing/ayqio - 码云 - 开源中国 (gitee.com)
asyncio.sleep
还有个参数是 result
,这个我们直接加上即可。
如果我们在多实现几个 api ,还可以直接使用 asyncio.sleep
来运行。
import ayqio
import asyncio
async def main():
print('Hello ...')
# 这里的 name 由 Future.__await__ 返回
# name = await ayqio.sleep(1, "World")
name = await asyncio.sleep(1, "World", loop=ayqio.get_event_loop())
print(f'... {name}!')
ayqio.run(main())