網站建設能帶來流量么建站小程序
什么是協(xié)程
協(xié)程(co-routine,又稱微線程、纖程)
是一種多方協(xié)同的工作方式。
協(xié)程不是進程或線程,
其執(zhí)行過程類似于 Python 函數調用,
Python 的 asyncio 模塊實現(xiàn)的異步IO編程框架中,
協(xié)程是對使用 async 關鍵字定義的異步函數的調用。
當前執(zhí)行者在某個時刻主動讓出(yield)控制流,
并記住自身當前的狀態(tài),
以便在控制流返回時能從上次讓出的位置恢復(resume)執(zhí)行。
一個進程包含多個線程,
類似于一個人體組織有多種細胞在工作,
同樣,一個程序可以包含多個協(xié)程。
多個線程相對獨立,
線程的切換受系統(tǒng)控制。
同樣,多個協(xié)程也相對獨立,
但是其切換由程序自己控制。
簡而言之,
協(xié)程的核心思想就在于執(zhí)行者對控制流的 “主動讓出” 和 “恢復”。
相對于,
線程此類的 “搶占式調度” 而言,
協(xié)程是一種 “協(xié)作式調度” 方式,
協(xié)程之間執(zhí)行任務按照一定順序交替執(zhí)行。
Python 對協(xié)程的支持經歷了多個版本:
- Python2.x 對協(xié)程的支持比較有限,通過 yield 關鍵字支持的生成器實現(xiàn)了一部分協(xié)程的功能但不完全。
- 第三方庫 gevent 對協(xié)程有更好的支持。
- Python3.4 中提供了 asyncio 模塊。
- Python3.5 中引入了 async/await 關鍵字。
- Python3.6 中 asyncio 模塊更加完善和穩(wěn)定。
- Python3.7 中內置了 async/await 關鍵字。
gevent 是對greenlet進行的封裝,
而greenlet 又是對yield進行封裝。
一、協(xié)程實現(xiàn)方法:
1、greenlet,早期模塊
greenlet包是一個Stackless(無棧化的)CPython版本,支持微線程(tasklet)。tasklet可以偽并行的運行并且同步的在信道上交換數據
①首先要先安裝greenlet模塊
pip install greenlet
from greenlet import greenlet
###免費領python源碼籽料qun:5403 05994
def func1():print(1) # 第1步 輸出1# 該方法遇到阻塞可以切換到函數2中進行使用gr2.switch() # 第2步:切換到func2中 并執(zhí)行print(2) # 第五步 輸出2gr2.switch() # 第六步 切換 func2def func2():print(3) # 第三步:輸出3gr1.switch() # 第四步:切換回func1 并執(zhí)行print(4) # 第七步:輸出4gr1 = greenlet(func1)
gr2 = greenlet(func2)gr1.switch() # 第0步,切換func1并執(zhí)行
運行結果:
2、yield關鍵字(Python2.x開始)
###免費領python源碼籽料qun:5403 05994
def func1():yield 1yield from func2()yield 2def func2():yield 3yield 4f1 = func1()
for item in f1:print(item)
運行結果:
這里可以思考對比一下yield和return
3、asyncio裝飾器(Python 3.4開始)
###免費領python源碼籽料qun:5403 05994
# asyncio(在python3.4之后的版本)
# 遇到IO等耗時操作會自動切換
import asyncio
import time@asyncio.coroutine
def func1():print(1)yield from asyncio.sleep(3) # 遇到耗時后會自動切換到其他函數中執(zhí)行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())
]# 協(xié)程函數使用 func1()這種方式是執(zhí)行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 執(zhí)行一個函數
end = time.time()
print(end - start) # 只會等待3秒
運行結果:
4、async、await關鍵字(Python 3.5開始)
###免費領python源碼籽料qun:5403 05994import asyncio
import timeasync def func1():print(1)await asyncio.sleep(3) # 遇到耗時后會自動切換到其他函數中執(zhí)行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())
]# 協(xié)程函數使用 func1()這種方式是執(zhí)行不了的
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# loop.run_until_complete(func1()) 執(zhí)行一個函數
end = time.time()
print(end - start) # 只會等待3秒
運行結果:
5、gevent
import geventdef 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])
運行結果:
gevent的優(yōu)勢不僅僅是在代碼中調用方便,
厲害的是它擁有的monkey機制。
假設你不愿意修改原來已經寫好的python代碼,
但是又想充分利用gevent機制,
那么你就可以用monkey來做到這一點。
你所要做的就是在文件開頭打一個patch,
那么它就會自動替換你原來的thread、socket、time、multiprocessing等代碼,
全部變成gevent框架。
這一切都是由gevent自動完成的。
注意這個patch是在所有module都import了之后再打,
否則沒有效果。
甚至在編寫的Web App代碼的時候,
不需要引入gevent的包,
也不需要改任何代碼,
僅僅在部署的時候,
用一個支持gevent的WSGI服務器,
就可以獲得數倍的性能提升。
二、協(xié)程的運行原理
當程序運行時,
操作系統(tǒng)會為每個程序分配一塊同等大小的虛擬內存空間,
并將程序的代碼和所有靜態(tài)數據加載到其中。
然后,創(chuàng)建和初始化 Stack 存儲,
用于儲存程序的局部變量,
函數參數和返回地址;
創(chuàng)建和初始化 Heap 內存;
創(chuàng)建和初始化 I/O 相關的任務。
當前期準備工作完成后,
操作系統(tǒng)將 CPU 的控制權移交給新創(chuàng)建的進程,
進程開始運行。
一個進程可以有一個或多個線程,
同一進程中的多個線程將共享該進程中的全部系統(tǒng)資源,
如:虛擬地址空間,文件描述符和信號處理等等。
但同一進程中的多個線程有各自的調用棧和線程本地存儲。
協(xié)程是一種比線程更加輕量級的存在,
協(xié)程不是被操作系統(tǒng)內核所管理,
而完全是由用戶態(tài)程序所控制。
協(xié)程與線程以及進程的關系如下圖所示。
可見,協(xié)程自身無法利用多核,
需要配合進程來使用才可以在多核平臺上發(fā)揮作用。
協(xié)程之間的切換不需要涉及任何 System Call(系統(tǒng)調用)或任何阻塞調用。
協(xié)程只在一個線程中執(zhí)行,切換由用戶態(tài)控制,而線程的阻塞狀態(tài)是由操作系統(tǒng)內核來完成的,因此協(xié)程相比線程節(jié)省線程創(chuàng)建和切換的開銷。
協(xié)程中不存在同時寫變量的沖突,因此,也就不需要用來守衛(wèi)關鍵區(qū)塊的同步性原語,比如:互斥鎖、信號量等,并且不需要來自操作系統(tǒng)的支持。
三、協(xié)程應用場景
1、搶占式調度的缺點
在 I/O 密集型場景中,
搶占式調度的解決方案是 “異步 + 回調” 機制。
其存在的問題是,
在某些場景中會使得整個程序的可讀性非常差。
以圖片下載為例,
圖片服務中臺提供了異步接口,
發(fā)起者請求之后立即返回,
圖片服務此時給了發(fā)起者一個唯一標識 ID,
等圖片服務完成下載后把結果放到一個消息隊列,
此時需要發(fā)起者不斷消費這個 MQ 才能拿到下載是否完成的結果。
可見,
整體的邏輯被拆分為了好幾個部分,
各個子部分都會存在狀態(tài)的遷移,
日后必然是 BUG 的高發(fā)地。
2、用戶態(tài)協(xié)同調度的優(yōu)勢
而隨著網絡技術的發(fā)展和高并發(fā)要求,
協(xié)程所能夠提供的用戶態(tài)協(xié)同調度機制的優(yōu)勢,
在網絡操作、文件操作、
數據庫操作、消息隊列操作等
重 I/O 操作場景中逐漸被挖掘。
協(xié)程將 I/O 的處理權從內核態(tài)的操作系統(tǒng)交還給用戶態(tài)的程序自身。用戶態(tài)程序在執(zhí)行 I/O 時,主動的通過 yield(讓出)CPU 的執(zhí)行權給其他協(xié)程,多個協(xié)程之間處于平等、對稱、合作的關系。
四、協(xié)程使用注意事項
協(xié)程只有和異步IO結合起來才能發(fā)揮出最大的威力
假設協(xié)程運行在線程之上,
并且協(xié)程調用了一個阻塞IO操作,這時候會發(fā)生什么?
實際上操作系統(tǒng)并不知道協(xié)程的存在,
它只知道線程,
因此在協(xié)程調用阻塞IO操作的時候,
操作系統(tǒng)會讓線程進入阻塞狀態(tài),
當前的協(xié)程和其它綁定在該線程之上的協(xié)程都會陷入阻塞而得不到調度。
因此,
在協(xié)程中盡量不要調用阻塞IO的方法,
比如打印,讀取文件,Socket接口等,
除非改為異步調用的方式,
并且協(xié)程只有在IO密集型的任務中才會發(fā)揮作用。