Python Asyncio 協程(一)

Jimmy Huang
9 min readFeb 6, 2020

--

前言: 最近在接觸 Telegram Robot API時,發現github上許多專案都有使用async def,這是之前比較少碰到的,也是python 3.5版後新增的 Async, Await功能

2021–03 Update 又寫了一篇更詳細介紹python的語法糖 有興趣的可以看

觀念介紹

Blocking & Non-blocking (阻塞和非阻塞)

  • 這概念是指 一個function執行後,會不會一直等return結果才做下一件事情,阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
  • 例如去買書,問老闆有沒有書,阻塞程序就會等return後的結果後才會進行下一步,非阻塞則是在問有沒有書的同時就去旁邊玩沙了,過一會兒再來看看有沒有return

Synchronous & Asynchronous (同步與非同步)

  • 同步和異步關注的是訊息通訊的機制
  • 同步,就是在發出一個調用時,在沒有得到結果之前,該調用就不返回。
    也就是,調用者主動等待這個調用的結果
  • 異步則是相反,調用在發出之後,這個調用就直接返回了,所以沒有返回結果。
  • 換句話說,當一個異步過程調用發出後,調用者不會立刻得到結果。而是在調用發出後,被調用者通過狀態
  • 以下有個很好的例子可以解釋同步/阻塞之概念(來源網路 - 作者不明)
老張愛喝茶,廢話不說,煮開水。
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
1.老張把水壺放到火上,立等水開。 (同步阻塞)
老張覺得自己有點傻
2.老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有。(同步非阻塞)
老張還是覺得自己有點傻,於是變高端了,買了把會響笛的那種水壺。水開之後,能大聲發出嘀的噪音。
3.老張把響水壺放到火上,立等水開。 (異步阻塞)
老張覺得這樣傻等意義不大
4.老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。 (異步非阻塞)老張覺得自己聰明了。所謂同步異步,只是對於水壺而言。普通水壺,同步;響水壺,異步。
雖然都能幹活,但響水壺可以在自己完工之後,提示老張水開了。這是普通水壺所不能及的。同步只能讓調用者去輪詢自己(情況2中),
造成老張效率的低下。所謂阻塞非阻塞,僅僅對於老張而言。立等的老張,阻塞;看電視的老張,非阻塞。情況1和情況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對於立等的老張沒有太大的意義。所以一般異步是配合非阻塞使用的,這樣才能發揮異步的效用。

Process & Thread

  • 進程(Process): 是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎,
  • 一個進程包含至少一個線程,也可以包含多個線程(Thread),是計算機內核使用的最小單位
  • 線程(Thread): 是最小的執行單位,而進程Process由至少一個線程組成,一個Process也可以含有多個Thread
  • 舉例:在作業系統上,你可以同時開Word, Excel, Line 等等不同的應用程式,這些是屬於不同Process,而Word裡面,可以同時使用"複製""貼上"引入""等不同的這功能,這些就是屬於Word這個Process下的各個threads
  • 進程(Process)之間的變數不會共用,基本上是分開的,就像你開Word和開Line,Line不會知道你在Word裏面的變數,在編寫程式時,如果需要共用(也就是Process之間需要通信),需要特別做處理,以Python來說,Mutiprocessing的lib裡面提供了底層的這些機制,像是Queue, Pipes等
  • 線程(Thread)之間的變數是可以共用的,這時候就產生了另外一個問題,Thread safe,也就是搶變數會不會打架,一般來說在線程獲取變數前,就會加鎖(Lock),Python在這邊還有一個額外的機制叫做GIL,本文不多說,有興趣的可以自己搜尋

Async

  • Async的機制和上面的thread, process完全不同,基本上是可以在one process & one thread下,就達到多工處理的目的,不需要靠os調配process和thread的調度(對於這三者的比較,有興趣的可以看這篇)
  • 基本上的概念是從generator裡面的yield from產生出來的,不過在python 3.5版後,官方直接支援async 和 await的寫法
  • 用最白話的說法是,今天用了asyncio的寫法,你就不是主動等待去獲得答案,而是等函式主動告知你答案(就是燒水壺那個例子)
  • Async基本上是用在IO密集型的程序(舉例,網路爬蟲,你大部分時間是在等response),在CPU密集型的程式,Async或者Thread都不能達到太好的效果

Asyncio的組成單位

  • Event Loop(事件循環): 負責Schedule調配各項Coroutine,有點像是中央指揮官,例如檢查各個Coroutine的response收到了沒有,現在哪個Coroutine釋放資源了,下件要幹嘛
  • Coroutine(協程): 在Event Loop下的一個任務單位,(背後原理是 generator的生成器),在on await的時候,就會釋放出系統資源回給event loop,Coroutine被安排時,會被包裝成Tasks
    協程可以看做是”能在中途中斷、中途返回值給其他協程、中途恢復、中途傳入參數的函數”,和一般的函數只能在起始傳入參數,不能中斷,而且最後返回值給父函數之後就結束的概念不一樣
    定義協程很簡單,只要在定義函數時再前面加入”async”這個關鍵字就行了
  • Futures: Event Loop在執行Coroutine時,會回傳結果,這些結果的集合就是Futures,(結果也有可能是Exception)
  • Async異步編程概念:
    1. 單process,單條thread,就能對IO密集的程序進行多工處理,不需要動用到OS調度
    2. 需要良好的編程技巧,使用await 釋放出資源
    3. 不要要使用阻塞block-function
    例如使用time.sleep或是一些socket操作,都是常見會block的函式,都必須換成asyncio裡面提供的non-block function

如何使用Asyncio實作?

幾個步驟 1.創建Event Loop 2.在Event Loop上註冊任務(Tasks)
以下例子來自於 這裡

import signal  
import sys
import asyncio
import aiohttp
import json
loop = asyncio.get_event_loop() #創建Event loop
client = aiohttp.ClientSession(loop=loop)#這邊引用aiohttp模塊,其實就是做一個把client的task註冊到loop的動作
async def get_json(client, url):
async with client.get(url) as response:
assert response.status == 200
return await response.read()
async def get_reddit_top(subreddit, client):
data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')
j = json.loads(data1.decode('utf-8'))
for i in j['data']['children']:
score = i['data']['score']
title = i['data']['title']
link = i['data']['url']
print(str(score) + ': ' + title + ' (' + link + ')')
print('DONE:', subreddit + '\n')def signal_handler(signal, frame):
loop.stop()
client.close()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)asyncio.ensure_future(get_reddit_top('python', client))
asyncio.ensure_future(get_reddit_top('programming', client))
asyncio.ensure_future(get_reddit_top('compsci', client))
loop.run_forever()

在實作上,這篇也講的很好,大家可以去看看

參考資料:(以下這幾篇都是蠻詳盡的)

https://www.zhihu.com/question/19732473
https://zhuanlan.zhihu.com/p/25228075
http://yangcongchufang.com/%E9%AB%98%E7%BA%A7python%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/python-process-thread.html

--

--