天天看點

python核心技術與實戰(十四):Asyncio實作并發14.1 Asyncio簡介14.2 Asyncio的工作原理14.3 Asyncio使用示例14.4 Asyncio的缺陷14.4 選擇多線程還是Asyncio總結

14. Asyncio實作并發

  • 14.1 Asyncio簡介
  • 14.2 Asyncio的工作原理
  • 14.3 Asyncio使用示例
  • 14.4 Asyncio的缺陷
  • 14.4 選擇多線程還是Asyncio
  • 總結

14.1 Asyncio簡介

多線程已經可以帶來較大的效率提升,那麼我們還需要asyncio的原因是:

  • 多線程運作過程容易被打斷,有可能出現race condition的情況
  • 線程切換本身存在一定的消耗,若I/O操作非常heavy,多線程很有可能滿足不了高效率、高品質的需求

sync和async的概念區分:

  • sync即同步,指操作一個接一個地執行,下一個操作必須等上一個操作完成後才能執行
  • async即異步,指不同的操作可以交替執行,如果其中某個操作被block了,程式并不會等待,而是會找出可執行的操作繼續執行

14.2 Asyncio的工作原理

Asyncio和python主程式一樣,隻有一個主線程,但是可以進行多個不同任務(特殊的future對象,可類比為多線程裡的多個線程),這些任務被一個event loop的對象所控制。

任務可分為兩種狀态:預備狀态和等待狀态

  • 預備狀态:任務目前空閑,但随時待命準備運作
  • 等待狀态:任務已經運作,但正在等待外部的操作完成,如I/O操作

event loop的執行過程:

  1. event loop會維護兩個任務清單,分别對應預備和等待這兩種狀态
  2. 選取預備狀态的一個任務(根據等待時間長短、占用資源等因素選取),使其運作直到該任務把控制權還給event loop為止
  3. 當接收到任務控制權後,event loop會根據其完成狀态把任務放到對應的預備或等待狀态的清單。周遊等待狀态的清單,檢視清單中的任務是否完成。已完成:放到預備狀态的清單;未完成:繼續放在等待狀态的清單
  4. 原先在預備狀态清單中的任務位置不變,因為它們仍未運作。當所有任務被重新放置在合适的清單後,新的一輪循環又開始了:event loop繼續從預備狀态的清單中選取一個任務使其執行…如此周而複始,直到所有任務都完成

對于asyncio來說,它的任務在運作時不會被外部的一些因素打斷,是以asyncio内的操作不會出現race condition的情況,就不需要擔心線程安全的問題了

14.3 Asyncio使用示例

以之前博文中下載下傳網站内容的任務為例,其使用Asyncio的寫法如下所示(省略異常處理操作):

import asyncio
import aiohttp
import time

async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print('Read {} from {}'.format(resp.content_length, url))

async def download_all(sites):
    tasks = [asyncio.create_task(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)

def main():
    sites = [
        'https://en.wikipedia.org/wiki/Portal:Arts',
        'https://en.wikipedia.org/wiki/Portal:History',
        'https://en.wikipedia.org/wiki/Portal:Society',
        'https://en.wikipedia.org/wiki/Portal:Biography',
        'https://en.wikipedia.org/wiki/Portal:Mathematics',
        'https://en.wikipedia.org/wiki/Portal:Technology',
        'https://en.wikipedia.org/wiki/Portal:Geography',
        'https://en.wikipedia.org/wiki/Portal:Science',
        'https://en.wikipedia.org/wiki/Computer_science',
        'https://en.wikipedia.org/wiki/Python_(programming_language)',
        'https://en.wikipedia.org/wiki/Java_(programming_language)',
        'https://en.wikipedia.org/wiki/PHP',
        'https://en.wikipedia.org/wiki/Node.js',
        'https://en.wikipedia.org/wiki/The_C_Programming_Language',
        'https://en.wikipedia.org/wiki/Go_(programming_language)'
    ]
    start_time = time.perf_counter()
    asyncio.run(download_all(sites))
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
    
if __name__ == '__main__':
    main()

## 輸出
Read 63153 from https://en.wikipedia.org/wiki/Java_(programming_language)
Read 31461 from https://en.wikipedia.org/wiki/Portal:Society
Read 23965 from https://en.wikipedia.org/wiki/Portal:Biography
Read 36312 from https://en.wikipedia.org/wiki/Portal:History
Read 25203 from https://en.wikipedia.org/wiki/Portal:Arts
Read 15160 from https://en.wikipedia.org/wiki/The_C_Programming_Language
Read 28749 from https://en.wikipedia.org/wiki/Portal:Mathematics
Read 29587 from https://en.wikipedia.org/wiki/Portal:Technology
Read 79318 from https://en.wikipedia.org/wiki/PHP
Read 30298 from https://en.wikipedia.org/wiki/Portal:Geography
Read 73914 from https://en.wikipedia.org/wiki/Python_(programming_language)
Read 62218 from https://en.wikipedia.org/wiki/Go_(programming_language)
Read 22318 from https://en.wikipedia.org/wiki/Portal:Science
Read 36800 from https://en.wikipedia.org/wiki/Node.js
Read 67028 from https://en.wikipedia.org/wiki/Computer_science
Download 15 sites in 0.062144195078872144 seconds
           

上述代碼中用到了Async和await關鍵字,表示所修飾的語句/函數是non-block的,這對應的便是上面提到的event loop的概念:若任務的執行過程中需要等待,則将其放入等待狀态的清單中,然後繼續執行預備狀态清單中的任務。

主函數中的asyncio.run(coro)是Asyncio的root call,表示拿到event loop,運作輸入的coro(即協程),直到它結束,最後關閉這個event loop。

Asyncio版本的函數download_all(),和之前多線程版本有很大差別:

tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)
           

這裡的asyncio.create_task(coro),表示對輸入的協程coro建立一個任務,安排它的執行,并傳回此任務對象。可以看到,對每個網站,都建立了一個對應的任務。

再往下看,asyncio.gather(*aws, loop=None, return_exception=False),則表示在event loop 中運作*aws序列中的所有任務。

14.4 Asyncio的缺陷

在實際工作中,要想用好Asyncio,必須得有相應的python庫支援。在之前的多線程例子中,我們用到的是requests庫,而在這裡使用的卻是aiohttp庫,原因就在于requests庫與Asyncio不相容,但aiohttp庫相容。但是相容問題會随着版本的問題逐漸減少。

此外,使用Asyncio使得我們在任務排程方面有更大的自主權,寫代碼時就得更加注意,否則容易出現錯誤。

比如,如果你需要await一系列的操作,就得使用asyncio.gather();如果隻是單個的future,則用asyncio.wait()就可以了。那麼,對于你的future,是想讓它run_until_complete()還是run_forever()呢?此類問題都是在面對具體問題時需要去考慮的。

14.4 選擇多線程還是Asyncio

在面對具體問題時,我們可以按照以下僞代碼的規範去選擇用多線程還是asyncio:

if io_bound:
    if io_slow:
        print('Use Asyncio')
    else:
        print('Use multi-threading')
else if cpu_bound:
    print('Use multi-processing')
           
  • 如果是I/O bound,并且I/O操作很慢,需要很多任務/線程協同實作,那麼選用Asyncio更合适(任務難度大)
  • 如果是I/O bound,但是I/O操作很快,且隻需要有限任務/線程協同實作,那麼選擇多線程就行(任務難度小)
  • 如果是CPU bound,則需要選用多程序來提高程式運作效率

總結

不同于多線程,Asyncio是單線程的,但其内部event loop的機制,使得它可以并發運作多個不同的任務,并且比多線程享有更大的自主要制權。

Asyncio中的任務,在運作過程中不會被打斷,是以不會出現race condition的情況。

在I/O heavy的情況下,Asyncio的運作效率比多線程更好。這是因為:

  • 任務和線程切換損耗:Asyncio内部任務切換的損耗,遠比線程切換的損耗要小
  • 任務和線程數量:Asyncio可以開啟的任務數,也遠比多線程中的線程數量多得多
python核心技術與實戰(十四):Asyncio實作并發14.1 Asyncio簡介14.2 Asyncio的工作原理14.3 Asyncio使用示例14.4 Asyncio的缺陷14.4 選擇多線程還是Asyncio總結