python實現協程(四)

        我們本節開始學習的python asyncio包,使用基於事件循環驅動的協程實現併發。這是python中最大,也是最具雄心壯志的庫之一。既然asyncio基於事件驅動,那麼讓我們首先來了解下事件驅動編程,再進入正題。

一. 事件驅動

1.1 單線程、多進程以及事件驅動編程模型的比較

        事件驅動編程是一種編程範式,程序的執行流程由外部事件來決定。它的特點是包含一個事件循環,當外部事件發生時,使用一種回調機制來觸發相應的處理,另外兩種常用的編程範式是(單線程同步)以及多進程編程。

        下圖對比了單線程、多線程以及事件驅動編程模型。如圖,這個程序有A/B/C個任務需要完成,每個任務在執行過程中都存在IO阻塞,阻塞的時間使用黑色塊表示。

        單線程同步模型中,多個任務按序執行。一旦某個任務因爲I/O而阻塞,其他所有的任務都必須等待,直到前面的任務完成之後它們才能依次執行。如果任務之間並沒有互相依賴的關係,但仍然需要互相等待的話這就使得程序不必要的降低了運行速度。

        多進程同步模型中,各個任務分別在獨立的進程中執行。進程由操作系統來管理,在多處理器系統上可以並行處理,或者在單處理器系統上交錯執行。與單線程同步程序相比,多進程的效率更高,但同時創建進程的資源消耗也比較大(多線程操作共享資源時,還需要考慮同步互斥機制,而且CPython解釋器無法利用計算機多核的特性)。

        事件驅動編程模型中,多個任務在一個單獨的線程中交錯執行。當遇到I/O操作時,註冊一個回調到事件循環中,然後當I/O操作完成時繼續執行。事件循環輪詢所有的事件,當事件到來時將它們分配給等待處理事件的回調函數。因此,一個任務在遇到IO阻塞時,可以讓步出CPU的使用權,讓其它任務繼續執行,而不是一直等待。事件驅動編程模型不需要關心線程安全問題。

       我們之前介紹的IO多路複用,使用的就是事件驅動編程模型,利用select/poll/epoll將IO事件交給系統內核監控,當某個IO描述符結束阻塞準備就緒時,就將其返回。此處不再贅述,有需要的讀者可以訪問 python實現IO多路複用 瞭解select/poll/epoll的用法。

1.2 協程的引入

        事件驅動編程模型有諸多好處,但在嵌套多層回調時,可讀性較差,出現異常排查也很困難,非常不利於後期的維護。於是,我們引入協程來解決上面的問題,允許我們採用同步的方式去編寫異步的代碼,使代碼的可讀性提升,既操作簡單,速度又快。協程使用單線程去切換任務,性能遠高於線程切換,且不需要加鎖,併發性高。

       進程、線程以及協程的關係可以使用下圖描述: 

        進程可以包含多個線程,多個線程共享進程的資源,因此線程比進程更輕量;而協程的本質是一個函數,一個線程可以包含多個協程,協程比線程更輕量。

1.3 相關概念

       下面的概念在 python處理併發導讀與目錄 中已詳細區分,此處只作爲回顧,簡單概述。

  • 併發:CPU在多個任務之間不斷切換,比如在一秒內CPU切換了100個進程,就可以認爲CPU的併發是100。
  • 並行:在多核CPU中,多個任務在不同的CPU上同時運行;並行數量和CPU數量是一致的。
  • 同步:必須等待前一個調用完成後,再開始新的的調用。
  • 異步:不必等待前一個操作的完成,就開始新的的調用。
  • 阻塞:調用函數的時候,當前線程被掛起。
  • 非阻塞:調用函數的時候,當前線程不會被掛起,而是立即返回結果(不管什麼樣的結果)。

二. asyncio模塊

        python3.4中引入asyncio模塊,創建協程函數時使用@asyncio.coroutine裝飾器裝飾。我們前面介紹的yield from是python3.4前的用法,即包含yield from語句的函數即可作爲生成器函數,也可以稱作協程函數。而在python3.4之後,使用@asyncio.coroutine裝飾器的函數即可稱作協程函數。關於asyncio中的基本概念總結如下:

術語 說明
coroutine 協程對象 使用@asyncio.coroutine裝飾器裝飾的函數被稱作協程函數,它的調用不會立即執行,而是返回一個協程對象。協程對象需要包裝成任務注入到事件循環,由事件循環調用。
task 任務 使用協程對象作爲參數創建任務,任務是協程對象的進一步封裝,其包含任務的各種狀態
event_loop 事件循環 協程函數必須添加到事件循環中,由事件循環去運行,因爲直接調用協程函數返回的是協程對象,協程函數並不會真正開始運行。事件循環控制任務運行流程,是任務的調用方。

示例 asyncio實現協程的簡單示例 

import time
import asyncio


@asyncio.coroutine
def do_some_work():
    print('Coroutine Start.')
    time.sleep(3)  # 模擬IO操作
    print('Print in coroutine.')


def main():
    start = time.time()
    loop = asyncio.get_event_loop()
    coroutine = do_some_work()
    loop.run_until_complete(coroutine)
    end = time.time()
    print('運行耗時:{:.2f}'.format(end - start))  # 打印程序運行耗時


if __name__ == '__main__':
    main()

運行結果:首先使用協程裝飾器@asyncio.coroutine創建協程函數,協程函數中使用time.sleep(3)模擬一個耗時的IO操作。asyncio.get_event_loop()用來創建事件循環;每個線程中只能有一個事件循環,get_event_loop獲取當前已經存在的事件循環,如果當前線程中沒有,則新建一個事件循環。loop.run_until_complete(coroutine) 將協程對象注入到事件循環,協程的運行由事件循環控制。事件循環的 run_until_complete 方法會阻塞運行,直到任務全部完成。協程對象作爲 run_until_complete 方法的參數,loop 會自動將協程對象包裝成任務來運行。下節我們會講到多個任務注入事件循環的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章