asyncio 学习(1)

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.sleepayqio.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 对象。同时我们将创建 FutureTask 封装到 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 来实现。
完整的流程如下

  1. run 函数根据传入的协程 main() 构建 Task ;
  2. Task 的实例化过程 __init__ 注册回调到事件循环,这个回调负责驱动协程,执行到 sleep 处的 await ;
  3. 开始事件循环,执行第 2 步中的回调,即 Task.__step 方法;
  4. Task.__step 方法驱动协程,执行到 await sleep 处;
  5. sleep 构建 Future ,注册时间事件 Future.set_result 到事件循环,返回构建好的 Future
  6. 控制流回到 Task.__step 方法, result 接收 await sleep 返回的 Future ,同时将下一次协程的执行 (Task.__wakeup) 注册到 Future 的回调;
  7. 控制流回到事件循环,开始下一次循环;
  8. 1s 过后,事件循环执行时间事件回调 Future.set_resultFuture 开始执行它的回调,即第 6 步中注册的 Task.__wakeup
  9. Task.__wakeup 读取执行结果,驱动协程执行下一步,Future.__await__ 方法被执行,返回 Futureresult ,回到 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())

你可能感兴趣的:(asyncio 学习(1))