天天看點

Python 協程詳解

什麼是協程      

       協程(co-routine,又稱微線程、纖程)是一種多方協同的工作方式。協程不是程序或線程,其執行過程類似于 Python 函數調用,Python 的 asyncio 子產品實作的異步IO程式設計架構中,協程是對使用 async 關鍵字定義的異步函數的調用。目前執行者在某個時刻主動讓出(yield)控制流,并記住自身目前的狀态,以便在控制流傳回時能從上次讓出的位置恢複(resume)執行。

       一個程序包含多個線程,類似于一個人體組織有多種細胞在工作,同樣,一個程式可以包含多個協程。多個線程相對獨立,線程的切換受系統控制。

       同樣,多個協程也相對獨立,但是其切換由程式自己控制。簡而言之,協程的核心思想就在于執行者對控制流的 “主動讓出” 和 “恢複”。相對于,線程此類的 “搶占式排程” 而言,協程是一種 “協作式排程” 方式,協程之間執行任務按照一定順序交替執行。

Python 協程詳解

Python 對協程的支援經曆了多個版本:

   Python2.x 對協程的支援比較有限,通過 yield 關鍵字支援的生成器實作了一部分協程的功能但不完全。

   第三方庫 gevent 對協程有更好的支援。

   Python3.4 中提供了 asyncio 子產品。

   Python3.5 中引入了 async/await 關鍵字。

   Python3.6 中 asyncio 子產品更加完善和穩定。

   Python3.7 中内置了 async/await 關鍵字。

gevent 是對greenlet進行的封裝,而greenlet 又是對yield進行封裝。

一、協程實作方法:

1、greenlet,早期子產品

  greenlet包是一個Stackless(無棧化的)CPython版本,支援微線程(tasklet)。tasklet可以僞并行的運作并且同步的在信道上交換資料。

①首先要先安裝greenlet子產品

pip install greenlet      
"""
* @Author: xiaofang
* @software: PyCharm
* @Description: 
"""
from greenlet import greenlet
 
 
def func1():
    print(1)  # 第1步 輸出1
    # 該方法遇到阻塞可以切換到函數2中進行使用
    gr2.switch()  # 第2步:切換到func2中 并執行
    print(2)  # 第五步 輸出2
    gr2.switch()  # 第六步 切換 func2
 
 
def func2():
    print(3)  # 第三步:輸出3
    gr1.switch()  # 第四步:切換回func1 并執行
    print(4)  # 第七步:輸出4
 
 
gr1 = greenlet(func1)
gr2 = greenlet(func2)
 
gr1.switch()  # 第0步,切換func1并執行      

運作結果:

Python 協程詳解

2、yield關鍵字(Python2.x開始)

"""
* @Author: xiaofang
* @software: PyCharm
* @Description: 
"""
 
 
def func1():
    yield 1
    yield from func2()
    yield 2
 
 
def func2():
    yield 3
    yield 4
 
 
f1 = func1()
for item in f1:
    print(item)      

運作結果:

Python 協程詳解

 這裡可以思考對比一下yield和return

3、asyncio​​裝飾器​​(Python 3.4開始)

"""
* @Author: xiaofang
* @software: PyCharm
* @Description: 
"""
# asyncio(在python3.4之後的版本)
# 遇到IO等耗時操作會自動切換
import asyncio
import time
 
 
@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(3)  # 遇到耗時後會自動切換到其他函數中執行
    print(2)
 
 
@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)
 
 
@asyncio.coroutine
def func3():
    print(5)
    yield from asyncio.sleep(2)
    print(6)
 
 
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
    asyncio.ensure_future(func3())
]
 
# 協程函數使用 func1()這種方式是執行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 執行一個函數
end = time.time()
print(end - start)  # 隻會等待3秒      

運作結果:

Python 協程詳解

4、​​async​​、await關鍵字(Python 3.5開始)

"""
* @Author: xiaofang
* @software: PyCharm
* @Description: 
"""
 
import asyncio
import time
 
 
async def func1():
    print(1)
    await asyncio.sleep(3)  # 遇到耗時後會自動切換到其他函數中執行
    print(2)
 
 
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
 
 
async def func3():
    print(5)
    await asyncio.sleep(2)
    print(6)
 
 
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
    asyncio.ensure_future(func3())
]
 
# 協程函數使用 func1()這種方式是執行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 執行一個函數
end = time.time()
print(end - start)  # 隻會等待3秒      

運作結果:

Python 協程詳解

5、gevent

"""
* @Author: xiaofang
* @software: PyCharm
* @Description: 
"""
 
import gevent
 
 
def f1():
    for i in range(1, 6):
        print('f1', i)
        gevent.sleep(0)
 
 
def f2():
    for i in range(6, 11):
        print('f2', i)
        gevent.sleep(0)
 
 
t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
gevent.joinall([t1, t2])      

運作結果:

Python 協程詳解

      gevent的優勢不僅僅是在代碼中調用友善,厲害的是它擁有的monkey機制。假設你不願意修改原來已經寫好的python代碼,但是又想充分利用gevent機制,那麼你就可以用monkey來做到這一點。

       你所要做的就是在檔案開頭打一個patch,那麼它就會自動替換你原來的thread、socket、time、multiprocessing等代碼,全部變成gevent架構。這一切都是由gevent自動完成的。注意這個patch是在所有module都import了之後再打,否則沒有效果。

       甚至在編寫的Web App代碼的時候,不需要引入gevent的包,也不需要改任何代碼,僅僅在部署的時候,用一個支援gevent的WSGI伺服器,就可以獲得數倍的性能提升。

二、協程的運作原理

      當程式運作時,作業系統會為每個程式配置設定一塊同等大小的虛拟記憶體空間,并将程式的代碼和所有靜态資料加載到其中。然後,建立和初始化 Stack 存儲,用于儲存程式的局部變量,函數參數和傳回位址;建立和初始化 Heap 記憶體;建立和初始化 I/O 相關的任務。目前期準備工作完成後,作業系統将 CPU 的控制權移交給新建立的程序,程序開始運作。

Python 協程詳解

一個程序可以有一個或多個線程,同一程序中的多個線程将共享該程序中的全部系統資源,如:虛拟位址空間,檔案描述符和信号處理等等。但同一程序中的多個線程有各自的調用棧和線程本地存儲。

Python 協程詳解

協程是一種比線程更加輕量級的存在,協程不是被作業系統核心所管理,而完全是由使用者态程式所控制。協程與線程以及程序的關系如下圖所示。可見,協程自身無法利用多核,需要配合程序來使用才可以在多核平台上發揮作用。

Python 協程詳解

協程之間的切換不需要涉及任何 System Call(系統調用)或任何阻塞調用。

協程隻在一個線程中執行,切換由使用者态控制,而線程的阻塞狀态是由作業系統核心來完成的,是以協程相比線程節省線程建立和切換的開銷。

協程中不存在同時寫變量的沖突,是以,也就不需要用來守衛關鍵區塊的同步性原語,比如:互斥鎖、信号量等,并且不需要來自作業系統的支援。

三、協程應用場景

1、搶占式排程的缺點

在 I/O 密集型場景中,搶占式排程的解決方案是 “異步 + 回調” 機制。

Python 協程詳解

其存在的問題是,在某些場景中會使得整個程式的可讀性非常差。以圖檔下載下傳為例,圖檔服務中台提供了異步接口,發起者請求之後立即傳回,圖檔服務此時給了發起者一個唯一辨別ID,等圖檔服務完成下載下傳後把結果放到一個消息隊列,此時需要發起者不斷消費這個 MQ 才能拿到下載下傳是否完成的結果。

Python 協程詳解

  可見,整體的邏輯被拆分為了好幾個部分,各個子部分都會存在狀态的遷移,日後必然是 BUG 的高發地。

Python 協程詳解

 2、使用者态協同排程的優勢

而随着網絡技術的發展和高并發要求,協程所能夠提供的使用者态協同排程機制的優勢,在網絡操作、檔案操作、資料庫操作、消息隊列操作等重 I/O 操作場景中逐漸被挖掘。

Python 協程詳解

四、協程使用注意事項   

  協程隻有和異步IO結合起來才能發揮出最大的威力    

       假設協程運作線上程之上,并且協程調用了一個阻塞IO操作,這時候會發生什麼?實際上作業系統并不知道協程的存在,它隻知道線程,是以在協程調用阻塞IO操作的時候,作業系統會讓線程進入阻塞狀态,目前的協程和其它綁定在該線程之上的協程都會陷入阻塞而得不到排程。