從頭造輪子:asyncio之 run_until_complete (1)

wilson排球發表於2021-12-20

前言

今天開始聊一聊python3的asyncio。關於asyncio,大家肯定都有自己的理解,並且網上大神也把基礎概念也解釋的比較透徹。
本文寫作的初衷,主要是理解asyncio的原理並且實現一遍。
話不多說,我們開始!

一、知識準備

● 理解程式、執行緒、協程。簡單來說,這三個都是為了解決多工同時進行的問題
  1)程式是操作資源分配的最小單位,多工的實現主要是極快地在程式間來回切換,而程式切換消耗時間最長(系統呼叫)
  2)執行緒依賴於程式,多個執行緒共享了父程式的一部分資源,執行緒切換時間相對於程式來說消耗時間大大減少,但是由於python gil的存在,並不存在多執行緒(系統呼叫)
  3)協程依賴於執行緒,由於程式/執行緒切換都是系統呼叫,開銷是巨大的。而協程是在使用者空間內完成任務切換,不會切換到作業系統資源(暫存器、訊號量、堆疊等),所以這種方式開銷最小。python的協程核心在於,遇到等待事件,就交出cpu控制權,轉而讓其他協程執行


● 理解python生成器,yield/yield from
  這裡就不班門弄斧了,直接推薦大佬的blog


● 理解關鍵字async/await,async/await是3.5之後的語法,和yield/yield from異曲同工


二、環境準備

元件 版本
python 3.7.7

三、run_until_complete的實現

先來看下官方asyncio的使用方法:

|># more main.py
import asyncio
async def hello():
    print('enter hello ...')
    return 'world'

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    task = loop.create_task(hello())
    rst = loop.run_until_complete(task)
    print(rst)
    
|># python3 main.py
enter hello ...
world

來看下造的輪子的使用方式:

▶ more main.py
from wilsonasyncio import get_event_loop
async def hello():
    print('enter hello ...')
    return 'world'


if __name__ == "__main__":
    loop = get_event_loop()
    task = loop.create_task(hello())
    rst = loop.run_until_complete(task)
    print(rst)
    
▶ python3 main.py
enter hello ...
world

自己造的輪子也很好的執行了,下面我們來看下輪子的程式碼

四、程式碼解析

輪子程式碼

1)程式碼組成

|># tree
.
├── eventloops.py 
├── futures.py
├── main.py
├── tasks.py
├── wilsonasyncio.py
檔案 作用
eventloops.py 事件迴圈
futures.py futures物件
tasks.py tasks物件
wilsonasyncio.py 可呼叫方法集合
main.py 入口

2)程式碼概覽:

eventloops.py

類/函式 方法 物件 作用 描述
Eventloop 事件迴圈,一個執行緒只有執行一個
__init__ 初始化兩個重要物件 self._readyself._stopping
self._ready 所有的待執行任務都是從這個佇列取出來,非常重要
self._stopping 事件迴圈完成的標誌
call_soon 呼叫該方法會立即將任務新增到待執行佇列
run_once run_forever呼叫,從self._ready佇列裡面取出任務執行
run_forever 死迴圈,若self._stopping則退出迴圈
run_until_complete 非常重要的函式,任務的起點和終點(後面詳細介紹)
create_task 將傳入的函式封裝成task物件,這個操作會將task.__step新增到__ready佇列
Handle 所有的任務進入待執行佇列(Eventloop.call_soon)之前都會封裝成Handle物件
__init__ 初始化兩個重要物件 self._callbackself._args
self._callback 待執行函式主體
self._args 待執行函式引數
_run 待執行函式執行
get_event_loop 獲取當前執行緒的事件迴圈
_complete_eventloop 將事件迴圈的_stopping標誌置位True

tasks.py

類/函式 方法 物件 作用 描述
Task 繼承自Future,主要用於整個協程執行的週期
__init__ 初始化物件 self._coro ,並且call_soonself.__step加入self._ready佇列
self._coro 使用者定義的函式主體
__step Task類的核心函式

futures.py

類/函式 方法 物件 作用 描述
Future 主要負責與使用者函式進行互動
__init__ 初始化兩個重要物件 self._loopself._callbacks
self._loop 事件迴圈
self._callbacks 回撥佇列,任務暫存佇列,等待時機成熟(狀態不是PENDING),就會進入_ready佇列
add_done_callback 新增任務回撥函式,狀態_PENDING,就虎進入_callbacks佇列,否則進入_ready佇列
set_result 獲取任務執行結果並儲存至_result,將狀態置位_FINISH,呼叫__schedule_callbacks
__schedule_callbacks 將回撥函式放入_ready,等待執行
result 獲取返回值

3)執行過程

3.1)入口函式

main.py

async def hello():
    print('enter hello ...')
    return 'world'

if __name__ == "__main__":
    loop = get_event_loop()
    task = loop.create_task(hello())
    rst = loop.run_until_complete(task)
    print(rst)
  • loop = get_event_loop()獲取事件迴圈
  • task = loop.create_task(hello())將使用者函式hello()封裝成協程,我們看下create_task的原始碼
    def create_task(self, coro):
        task = tasks.Task(coro, loop=self)
        return task

    初始化一個Task物件,從程式碼概覽得知,初始化物件之後會立即將__step新增到_ready佇列等待執行

  • rst = loop.run_until_complete(task)開始執行事件迴圈的第一個函式run_until_complete

3.2)事件迴圈啟動

eventloops.py

    def run_until_complete(self, future):
        future.add_done_callback(_complete_eventloop, future)
        self.run_forever()
        return future.result()
  • future.add_done_callback(_complete_eventloop, future)future新增回撥函式(future就是task物件,而task物件裡的任務就是hello() ),回撥函式是_complete_eventloop 。就是future執行完成之後執行_complete_eventloop
  • self.run_forever()啟動事件迴圈

3.3)第一次迴圈run_forever --> run_once

eventloops.py

    def run_once(self):
        ntodo = len(self._ready)
        for _ in range(ntodo):
            handle = self._ready.popleft()
            handle._run()
  • _ready佇列的內容(task.__step)取出來執行

tasks.py

    def __step(self, exc=None):
        coro = self._coro
        try:
            if exc is None:
                coro.send(None)
            else:
                coro.throw(exc)
        except StopIteration as exc:
            super().set_result(exc.value)
        finally:
            self = None
  • coro.send(None)核心程式碼,跳轉回到使用者函式hello()

main.py

async def hello():
    print('enter hello ...')
    return 'world'
  • 使用者函式非常簡單,列印一行資料,以及返回一個字串world,執行完成之後回到task.__step()
  • super().set_result(exc.value)由於使用者函式執行完成,會丟擲異常StopIteration,捕獲之後執行set_result
  • 由程式碼概覽得知set_result 的作用在於將任務狀態置位_FINISHED,並且將回撥函式(_complete_eventloop )寫入_ready佇列

3.4)第二次迴圈run_forever --> run_once

eventloops.py

    def run_once(self):
        ntodo = len(self._ready)
        for _ in range(ntodo):
            handle = self._ready.popleft()
            handle._run()
  • 繼續迴圈,handle封裝了_complete_eventloop
def _complete_eventloop(fut):
    fut._loop.stop()
  • 呼叫stop,設定停止標誌

3.5)第三次迴圈run_forever

    def run_forever(self):
        while True:
            self.run_once() 
            if self._stopping:
                break
  • 跳出事件迴圈,回到run_until_complete
    def run_until_complete(self, future):
        future.add_done_callback(_complete_eventloop, future)
        self.run_forever()
        return future.result()

3.6)回到主函式,獲取返回值

if __name__ == "__main__":
    loop = get_event_loop()
    task = loop.create_task(hello())
    rst = loop.run_until_complete(task)
    print(rst)
  • rst = loop.run_until_complete(task)獲取返回值

3.7)執行結果

▶ python3 main.py
enter hello ...
return world ...

五、流程總結

六、小結

● task物件與future有什麼區別?主要用於整個協程執行的週期,主要負責與使用者函式進行互動
● 本文從asyncio的第一個函式run_until_complete,介紹了asyncio的基本流程:使用者函式並不是立即執行,而是進入佇列,然後根據eventloop在合適的時機進行統一排程
● 本文中的程式碼,參考了python 3.7.7中asyncio的原始碼,裁剪而來
● 本文中程式碼:程式碼



至此,本文結束
在下才疏學淺,有撒湯漏水的,請各位不吝賜教...

相關文章