理解 Python 協程-async、await

看到吐血 (´ཀ`」 ∠)

  • 協程(Coroutine)本質上是一個函數,特點是在代碼塊中可以將執行權交給其他協程
  • 衆所周知,子程序(函數)都是層級調用的,如果在A中調用了B,那麼B執行完畢返回後A才能執行完畢。協程與子程序有點類似,但是它在執行過程中可以中斷,轉而執行其他的協程,在適當的時候再回來繼續執行。
  • 協程與多線程相比的最大優勢在於:協程是一個線程中執行,沒有線程切換的開銷;協程由用戶決定在哪裏交出控制權
  • 這裏用到的是asyncio庫(Python 3.7),這個庫包含了大部分實現協程的魔法工具
    • 使用 async 修飾詞聲明異步函數
    • 使用 await 語句執行可等待對象(Coroutine、Task、Future)
    • 使用 asyncio.create_task 創建任務,將異步函數(協程)作爲參數傳入,等待event loop執行
    • 使用 asyncio.run 函數運行協程程序,協程函數作爲參數傳入

解析協程運行時

import asyncio
import time

async def a():
    print("歡迎使用 a !")
    await asyncio.sleep(1)
    print("歡迎回到 a !")

async def b():
    print("歡迎來到 b !")
    await asyncio.sleep(2)
    print("歡迎回到 b !")

async def main():
    task1 = asyncio.create_task(a())
    task2 = asyncio.create_task(b())
    print("準備開始")
    await task1
    print("task1 結束")
    await task2
    print("task2 結束")

if __name__ == "__main__":
    start = time.perf_counter()
    
    asyncio.run(main())
    
    print('花費 {} s'.format(time.perf_counter() - start))

  • 解釋:
    • 1、asyncio.run(main()),程序進入main()函數,開啓事件循環
    • 2、創建任務task1、task2並進入事件循環等待運行
    • 3、輸出準備開始
    • 4、執行await task1,用戶選擇從當前主任務中切出,事件調度器開始調度 a
    • 5、a 開始運行,輸出歡迎使用a!,運行到await asyncio.sleep(1),從當前任務切出,事件調度器開始調度 b
    • 6、b 開始運行,輸出歡迎來到b!,運行到await asyncio.sleep(2),從當前任務切出
    • 7、以上事件運行時間非常短(毫秒),事件調度器開始暫停調度
    • 8、一秒鐘後,a的sleep完成,事件調度器將控制權重新交給a,輸出歡迎回到a!,task1完成任務,退出事件循環
    • 9、await task1完成,事件調度器將控制權還給主任務,輸出task1結束,然後在await task2處繼續等待
    • 10、兩秒鐘後,b的sleep完成,事件調度器將控制權重新傳給 b,輸出歡迎回到 b!,task2完成任務,從事件循環中退出
    • 11、事件調度器將控制權交還給主任務,主任務輸出task2結束,至此協程任務全部結束,事件循環結束。

上面的代碼也可以這樣寫,將15到21行換成一行await asyncio.gather(a(), b())也能實現類似的效果,await asyncio.gather 會併發運行傳入的可等待對象(Coroutine、Task、Future)。

import asyncio
import time

async def a():
    print("歡迎使用 a !")
    await asyncio.sleep(1)
    print("歡迎回到 a !")

async def b():
    print("歡迎來到 b !")
    await asyncio.sleep(2)
    print("歡迎回到 b !")

async def main():
    await asyncio.gather(a(), b())

if __name__ == "__main__":
    start = time.perf_counter()
    
    asyncio.run(main())
    
    print('花費 {} s'.format(time.perf_counter() - start))

異步接口同步實現

"""
- 簡單爬蟲模擬
- 這裏用異步接口寫了個同步代碼
"""

import asyncio
import time

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)  # 休眠
    print('OK {}'.format(url))

async def main(urls):
    for url in urls:
        await crawl_page(url)  # await會將程序阻塞在這裏,進入被調用的協程函數,執行完畢後再繼續


start = time.perf_counter()

# pip install nest-asyncio
asyncio.run(main(['url_1', 'url_2'])) # 協程接口

print("Cost {} s".format(time.perf_counter() - start))

使用Task實現異步

# 異步實現

import asyncio
import time

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task
    # 14、15行也可以換成這一行await asyncio.gather(*tasks)
    # *tasks 解包列表,將列表變成了函數的參數,與之對應的是,** dict 將字典變成了函數的參數

start = time.perf_counter()

asyncio.run(main(['url_1', 'url_2']))

print("Cost {} s".format(time.perf_counter() - start))

生產者消費者模型的協程版本

在這裏插入圖片描述

# 極客時間:Python核心技術與實戰

import asyncio
import random
import time

async def consumer(queue, id):
    """消費者"""
    while True:
        val = await queue.get()
        print('{} get a val : {}'.format(id, val))
        await asyncio.sleep(1)


async def producer(queue, id):
    """生產者"""
    for _ in range(5):
        val = random.randint(1, 10)
        await queue.put(val)
        print('{} put a val: {}'.format(id, val))
        await asyncio.sleep(1)

async def main():
    queue = asyncio.Queue()
    
    consumer_1 = asyncio.create_task(consumer(queue, 'consumer_1'))
    consumer_2 = asyncio.create_task(consumer(queue, 'consumer_2'))

    producer_1 = asyncio.create_task(producer(queue, 'producer_1'))
    producer_2 = asyncio.create_task(producer(queue, 'producer_2'))
    
    await asyncio.sleep(10)
    # cancel掉執行之間過長的consumer_1、consumer_2,while True
    consumer_1.cancel()
    consumer_2.cancel()
    
    # return_exceptions 設爲True,不讓異常throw到執行層,影響後續任務的執行
    await asyncio.gather(consumer_1, consumer_2, producer_1, producer_2, return_exceptions=True)

if __name__ == "__main__":
    start = time.perf_counter()

    asyncio.run(main())

    print("Cost {} s".format(time.perf_counter() - start))

在這裏插入圖片描述

# 輸出結果
producer_1 put a val: 7
producer_2 put a val: 4
consumer_1 get a val : 7
consumer_2 get a val : 4
producer_1 put a val: 6
producer_2 put a val: 1
consumer_2 get a val : 6
consumer_1 get a val : 1
producer_1 put a val: 8
producer_2 put a val: 1
consumer_1 get a val : 8
consumer_2 get a val : 1
producer_1 put a val: 6
producer_2 put a val: 4
consumer_2 get a val : 6
consumer_1 get a val : 4
producer_1 put a val: 7
producer_2 put a val: 8
consumer_1 get a val : 7
consumer_2 get a val : 8
Cost 10.0093015 s

拓展閱讀:Python的生產者消費者模型,看這篇就夠了

參考

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