asyncio是官方提供的協(xié)程的類庫,從python3.4開始支持該模塊
創(chuàng)新互聯(lián)公司是一家專業(yè)提供徐水企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、成都網(wǎng)站制作、H5建站、小程序制作等業(yè)務(wù)。10年已為徐水眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進(jìn)行中。
async awiat是python3.5中引入的關(guān)鍵字,使用async關(guān)鍵字可以將一個(gè)函數(shù)定義為協(xié)程函數(shù),使用awiat關(guān)鍵字可以在遇到IO的時(shí)候掛起當(dāng)前協(xié)程(也就是任務(wù)),去執(zhí)行其他協(xié)程。
await + 可等待的對(duì)象(協(xié)程對(duì)象、Future對(duì)象、Task對(duì)象 - IO等待)
注意:在python3.4中是通過asyncio裝飾器定義協(xié)程,在python3.8中已經(jīng)移除了asyncio裝飾器。
事件循環(huán),可以把他當(dāng)做是一個(gè)while循環(huán),這個(gè)while循環(huán)在周期性的運(yùn)行并執(zhí)行一些協(xié)程(任務(wù)),在特定條件下終止循環(huán)。
loop = asyncio.get_event_loop():生成一個(gè)事件循環(huán)
loop.run_until_complete(任務(wù)):將任務(wù)放到事件循環(huán)
Tasks用于并發(fā)調(diào)度協(xié)程,通過asyncio.create_task(協(xié)程對(duì)象)的方式創(chuàng)建Task對(duì)象,這樣可以讓協(xié)程加入事件循環(huán)中等待被調(diào)度執(zhí)行。除了使用 asyncio.create_task() 函數(shù)以外,還可以用低層級(jí)的 loop.create_task() 或 ensure_future() 函數(shù)。不建議手動(dòng)實(shí)例化 Task 對(duì)象。
本質(zhì)上是將協(xié)程對(duì)象封裝成task對(duì)象,并將協(xié)程立即加入事件循環(huán),同時(shí)追蹤協(xié)程的狀態(tài)。
注意:asyncio.create_task() 函數(shù)在 Python 3.7 中被加入。在 Python 3.7 之前,可以改用 asyncio.ensure_future() 函數(shù)。
下面結(jié)合async awiat、事件循環(huán)和Task看一個(gè)示例
示例一:
*注意:python 3.7以后增加了asyncio.run(協(xié)程對(duì)象),效果等同于loop = asyncio.get_event_loop(),loop.run_until_complete(協(xié)程對(duì)象) *
示例二:
注意:asyncio.wait 源碼內(nèi)部會(huì)對(duì)列表中的每個(gè)協(xié)程執(zhí)行ensure_future從而封裝為Task對(duì)象,所以在和wait配合使用時(shí)task_list的值為[func(),func()] 也是可以的。
示例三:
在學(xué)習(xí)gevent之前,你肯定要知道你學(xué)的這個(gè)東西是什么。
官方描述gevent
gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
翻譯:gevent是一個(gè)基于協(xié)程的Python網(wǎng)絡(luò)庫。我們先理解這句,也是這次學(xué)習(xí)的重點(diǎn)——協(xié)程。
wiki描述協(xié)程
與子例程一樣,協(xié)程也是一種程序組件。相對(duì)子例程而言,協(xié)程更為一般和靈活,但在實(shí)踐中使用沒有子例程那樣廣泛。子例程的起始處是惟一的入口點(diǎn),一旦退出即完成了子例程的執(zhí)行,子例程的一個(gè)實(shí)例只會(huì)返回一次;協(xié)程可以通過yield來調(diào)用其它協(xié)程。通過yield方式轉(zhuǎn)移執(zhí)行權(quán)的協(xié)程之間不是調(diào)用者與被調(diào)用者的關(guān)系,而是彼此對(duì)稱、平等的。協(xié)程允許多個(gè)入口點(diǎn),可以在指定位置掛起和恢復(fù)執(zhí)行。
沒看懂?沒關(guān)系,我也沒看懂,不過算是有點(diǎn)線索:子例程。
子例程
過程有兩種,一種叫子例程(Subroutine),通常叫Sub;另一種叫函數(shù)(Function)。底層實(shí)現(xiàn)機(jī)制是一樣的,區(qū)別在于,Sub只執(zhí)行操作,沒有返回值;Function不但執(zhí)行操作,并且有返回值。用過VB的應(yīng)該會(huì)比較清楚這點(diǎn)。(原諒我用了百度百科)說到底子例程就是過程,我們一般叫它函數(shù)。
說到函數(shù),我就想吐槽了,不明白為什么要叫函數(shù)。很多時(shí)候我們寫一個(gè)函數(shù)是為了封裝、模塊化某個(gè)功能,它是一個(gè)功能、或者說是一個(gè)過程。因?yàn)樗氖穷愃朴诹鞒虉D那樣的具體邏輯,先怎樣做,然后怎樣做;如果遇到A情況則怎樣,如果遇到B情況又怎樣。個(gè)人覺得還是叫過程比較好,叫做函數(shù)就讓人很糾結(jié)了,難道因?yàn)榛貧w到底層還是計(jì)算問題,出于數(shù)學(xué)的角度把它稱為函數(shù)?這個(gè)略坑??!為了符合大家的口味,我還是稱之為函數(shù)好了(其實(shí)我也習(xí)慣叫函數(shù)了%_
講到函數(shù),我們就往底層深入一點(diǎn),看看下面的代碼:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def a():
print "a start"
b()
print "a end"
def b():
print "b start"
c()
print "b end"
def c():
print "c start"
print "c end"
if __name__ == "__main__":
a()
a start
b start
c start
c end
b end
a end
對(duì)于這樣的結(jié)果大家肯定不會(huì)意外的。每當(dāng)函數(shù)被調(diào)用,就會(huì)在棧中開辟一個(gè)??臻g,調(diào)用結(jié)束后再回收該空間。
假設(shè)一個(gè)這樣的場(chǎng)景:有個(gè)講臺(tái),每個(gè)人都可以上去發(fā)表言論,但是每次講臺(tái)只能站一個(gè)人?,F(xiàn)在a在上面演講,當(dāng)他說到“大家好!”的時(shí)候,b有個(gè)緊急通知要告訴大家,所以a就先下來讓b講完通知,然后a再上講臺(tái)繼續(xù)演講。如果用函數(shù)的思想模擬這個(gè)問題,堆棧示意圖是這樣的:
大家會(huì)不會(huì)發(fā)現(xiàn)問題,就是b通知完a繼續(xù)演講都要重新開始。因?yàn)楹瘮?shù)在重新調(diào)用的時(shí)候,它的局部變量是會(huì)被重置的,對(duì)于之前他說的那句“大家好”,他是不會(huì)記得的(可能a的記性不好)。那有沒有什么辦法可以不讓他重復(fù),而是在打斷之后繼續(xù)呢?很簡(jiǎn)單,在他走下講臺(tái)之前記住當(dāng)前說過的話。表現(xiàn)在函數(shù)中就是在退出之前,保存該函數(shù)的局部變量,方便在重新進(jìn)入該函數(shù)的時(shí)候,能夠從之前的局部變量開始繼續(xù)執(zhí)行。
升級(jí)版
如果你有一段代碼生產(chǎn)數(shù)據(jù),另外一段代碼消費(fèi)數(shù)據(jù),哪個(gè)應(yīng)該是調(diào)用者,哪個(gè)應(yīng)該是被調(diào)用者?
例如:生產(chǎn)者 —— 消費(fèi)者問題,先拋開進(jìn)程、線程等實(shí)現(xiàn)方法。假設(shè)有兩個(gè)函數(shù)producer和consumer,當(dāng)緩沖區(qū)滿了,producer調(diào)用consumer,當(dāng)緩沖區(qū)空了,consumer調(diào)用producer,但是這樣的函數(shù)互相調(diào)用會(huì)出什么問題?
Python
1
2
3
4
5
6
7
8
def producer():
print "生產(chǎn)一個(gè)"
consumer()
def consumer():
print "消費(fèi)一個(gè)"
producer()
producer生產(chǎn)一個(gè),緩沖區(qū)滿了,consumer消費(fèi)一個(gè),緩沖區(qū)空了,producer生產(chǎn)一個(gè),如此循環(huán)。會(huì)看到下面這樣的圖:
看起來好像不錯(cuò),感覺兩個(gè)函數(shù)協(xié)調(diào)運(yùn)行的很好,很好的解決了生產(chǎn)者——消費(fèi)者問題。如果真有這么好也就不會(huì)有協(xié)程的存在了,仔細(xì)分析會(huì)有兩個(gè)問題:
無限次數(shù)的函數(shù)嵌套調(diào)用,而沒有函數(shù)返回,會(huì)有什么樣的后果?
兩個(gè)函數(shù)貌似協(xié)調(diào)有序的工作,你來我往,但每次執(zhí)行的都是同一個(gè)函數(shù)實(shí)例嗎?
首先,上面的偽代碼示例是一個(gè)無限的函數(shù)嵌套調(diào)用,沒有函數(shù)返回來釋放棧,棧的空間不斷的在增長(zhǎng),直到溢出,程序崩潰。然后,看起來兩個(gè)函數(shù)協(xié)調(diào)有序,事實(shí)上操作的都不是同一個(gè)實(shí)例對(duì)象,不知道下面的圖能否看懂。
那什么東西有這樣的能力呢?我們很快就可以想到進(jìn)程、線程,但是你真的想使用進(jìn)程、線程如此重量級(jí)的東西在這么簡(jiǎn)單的程序上嗎?野蠻的搶占式機(jī)制和笨重的上下文切換!
還有一種程序組件,那就是協(xié)程。它能保留上一次調(diào)用時(shí)的狀態(tài),每次重新進(jìn)入該過程的時(shí)候,就相當(dāng)于回到上一次離開時(shí)所處邏輯流的位置。協(xié)程的起始處是第一個(gè)入口點(diǎn),在協(xié)程里,返回點(diǎn)之后是接下來的入口點(diǎn)。協(xié)程的生命期完全由他們的使用的需要決定。每個(gè)協(xié)程在用yield命令向另一個(gè)協(xié)程交出控制時(shí)都盡可能做了更多的工作,放棄控制使得另一個(gè)協(xié)程從這個(gè)協(xié)程停止的地方開始,接下來的每次協(xié)程被調(diào)用時(shí),都是從協(xié)程返回(或yield)的位置接著執(zhí)行。
從上面這些你就可以知道其實(shí)協(xié)程是模擬了多線程(或多進(jìn)程)的操作,多線程在切換的時(shí)候都會(huì)有一個(gè)上下文切換,在退出的時(shí)候?qū)F(xiàn)場(chǎng)保存起來,等到下一次進(jìn)入的時(shí)候從保存的現(xiàn)場(chǎng)開始,繼續(xù)執(zhí)行。
看下協(xié)程是怎樣實(shí)現(xiàn)的:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import random
from time import sleep
from greenlet import greenlet
from Queue import Queue
queue = Queue(1)
@greenlet
def producer():
chars = ['a', 'b', 'c', 'd', 'e']
global queue
while True:
char = random.choice(chars)
queue.put(char)
print "Produced: ", char
sleep(1)
consumer.switch()
@greenlet
def consumer():
global queue
while True:
char = queue.get()
print "Consumed: ", char
sleep(1)
producer.switch()
if __name__ == "__main__":
producer.run()
consumer.run()
應(yīng)用場(chǎng)景
我們一直都在大談協(xié)程是什么樣一個(gè)東西,卻從沒有提起協(xié)程用來干嘛,這個(gè)其實(shí)大家分析一下就能夠知道。從上面的生產(chǎn)者——消費(fèi)者問題應(yīng)該能看出,它分別有兩個(gè)任務(wù),假設(shè)交給兩個(gè)人去執(zhí)行,但每次只能允許一個(gè)人行動(dòng)。當(dāng)緩沖區(qū)滿的時(shí)候,生產(chǎn)者是出于等待狀態(tài)的,這個(gè)時(shí)候可以將執(zhí)行任務(wù)的權(quán)利轉(zhuǎn)交給消費(fèi)者,當(dāng)緩沖區(qū)空得時(shí)候,消費(fèi)者是出于等待狀態(tài)的,這個(gè)時(shí)候可以將執(zhí)行任務(wù)的權(quán)利轉(zhuǎn)交給生產(chǎn)者,是不是很容易聯(lián)想到多任務(wù)切換?然后想到線程?最后想到高并發(fā)?
但同學(xué)們又會(huì)問,既然有了線程為什么還要協(xié)程呢?因?yàn)榫€程是系統(tǒng)級(jí)別的,在做切換的時(shí)候消耗是特別大的,具體為什么這么大等我研究好了再告訴你;同時(shí)線程的切換是由CPU決定的,可能你剛好執(zhí)行到一個(gè)地方的時(shí)候就要被迫終止,這個(gè)時(shí)候你需要用各種措施來保證你的數(shù)據(jù)不出錯(cuò),所以線程對(duì)于數(shù)據(jù)安全的操作是比較復(fù)雜的。而協(xié)程是用戶級(jí)別的切換,且切換是由自己控制,不受外力終止。
總結(jié)
協(xié)程其實(shí)模擬了人類活動(dòng)的一種過程。例如:你準(zhǔn)備先寫文檔,然后修復(fù)bug。這時(shí)候接到電話說這個(gè)bug很嚴(yán)重,必須立即修復(fù)(可以看作CPU通知)。于是你暫停寫文檔,開始去填坑,終于你把坑填完了,你回來寫文檔,這個(gè)時(shí)候你肯定是接著之前寫的文檔繼續(xù),難道你要把之前寫的給刪了,重新寫?這就是協(xié)程。那如果是子例程呢?那你就必須重新寫了,因?yàn)橥顺鲋?,棧幀就?huì)被彈出銷毀,再次調(diào)用就是開辟新的??臻g了。
總結(jié):協(xié)程就是用戶態(tài)下的線程,是人們?cè)谟辛诉M(jìn)程、線程之后仍覺得效率不夠,而追求的又一種高并發(fā)解決方案。為什么說是用戶態(tài),是因?yàn)椴僮飨到y(tǒng)并不知道它的存在,它是由程序員自己控制、互相協(xié)作的讓出控制權(quán)而不是像進(jìn)程、線程那樣由操作系統(tǒng)調(diào)度決定是否讓出控制權(quán)。
協(xié)程函數(shù):async def?函數(shù)名。3.5+
協(xié)程對(duì)象:執(zhí)行協(xié)程函數(shù)()得到的協(xié)程對(duì)象。
3.5之后的寫法:
3.7之后的寫法:更簡(jiǎn)便
await后面?跟?可等待的對(duì)象。(協(xié)程對(duì)象,F(xiàn)uture,Task對(duì)象?約等于IO等待)
await實(shí)例2:串行執(zhí)行。 一個(gè)協(xié)程函數(shù)里面可以支持多個(gè)await ,雖然會(huì)串行,但是如果有其他協(xié)程函數(shù),任務(wù)列表也在執(zhí)行,依然會(huì)切換。只是案例中的main對(duì)應(yīng)執(zhí)行的others1和others2串行 。 await會(huì)等待對(duì)象的值得到之后才繼續(xù)往下走。
協(xié)程也稱為微線程,是在一個(gè)線程中,通過不斷的切換任務(wù)函數(shù)實(shí)現(xiàn)了多任務(wù)的效果。
協(xié)程在python實(shí)現(xiàn)的原理主要是通過yield這個(gè)關(guān)鍵字實(shí)現(xiàn)
但是真正在開發(fā)時(shí),可以不需要自己實(shí)現(xiàn),可以通過很多成熟的第三方模塊來實(shí)現(xiàn)協(xié)程,比如greenlet,gevent等模塊。多線程的課程我記得是在黑馬程序員里面找的,一套,還有資料。
python里推薦用多進(jìn)程而不是多線程,但是多進(jìn)程也有其自己的限制:相比線程更加笨重、切換耗時(shí)更長(zhǎng),并且在python的多進(jìn)程下,進(jìn)程數(shù)量不推薦超過CPU核心數(shù)(一個(gè)進(jìn)程只有一個(gè)GIL,所以一個(gè)進(jìn)程只能跑滿一個(gè)CPU),因?yàn)橐粋€(gè)進(jìn)程占用一個(gè)CPU時(shí)能充分利用機(jī)器的性能,但是進(jìn)程多了就會(huì)出現(xiàn)頻繁的進(jìn)程切換,反而得不償失。
不過特殊情況(特指IO密集型任務(wù))下,多線程是比多進(jìn)程好用的。
舉個(gè)例子:給你200W條url,需要你把每個(gè)url對(duì)應(yīng)的頁面抓取保存起來,這種時(shí)候,單單使用多進(jìn)程,效果肯定是很差的。為什么呢?
例如每次請(qǐng)求的等待時(shí)間是2秒,那么如下(忽略cpu計(jì)算時(shí)間):
1、單進(jìn)程+單線程:需要2秒*200W=400W秒==1111.11個(gè)小時(shí)==46.3天,這個(gè)速度明顯是不能接受的
2、單進(jìn)程+多線程:例如我們?cè)谶@個(gè)進(jìn)程中開了10個(gè)多線程,比1中能夠提升10倍速度,也就是大約4.63天能夠完成200W條抓取,請(qǐng)注意,這里的實(shí)際執(zhí)行是:線程1遇見了阻塞,CPU切換到線程2去執(zhí)行,遇見阻塞又切換到線程3等等,10個(gè)線程都阻塞后,這個(gè)進(jìn)程就阻塞了,而直到某個(gè)線程阻塞完成后,這個(gè)進(jìn)程才能繼續(xù)執(zhí)行,所以速度上提升大約能到10倍(這里忽略了線程切換帶來的開銷,實(shí)際上的提升應(yīng)該是不能達(dá)到10倍的),但是需要考慮的是線程的切換也是有開銷的,所以不能無限的啟動(dòng)多線程(開200W個(gè)線程肯定是不靠譜的)
3、多進(jìn)程+多線程:這里就厲害了,一般來說也有很多人用這個(gè)方法,多進(jìn)程下,每個(gè)進(jìn)程都能占一個(gè)cpu,而多線程從一定程度上繞過了阻塞的等待,所以比單進(jìn)程下的多線程又更好使了,例如我們開10個(gè)進(jìn)程,每個(gè)進(jìn)程里開20W個(gè)線程,執(zhí)行的速度理論上是比單進(jìn)程開200W個(gè)線程快10倍以上的(為什么是10倍以上而不是10倍,主要是cpu切換200W個(gè)線程的消耗肯定比切換20W個(gè)進(jìn)程大得多,考慮到這部分開銷,所以是10倍以上)。
還有更好的方法嗎?答案是肯定的,它就是:
4、協(xié)程,使用它之前我們先講講what/why/how(它是什么/為什么用它/怎么使用它)
what:
協(xié)程是一種用戶級(jí)的輕量級(jí)線程。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧。因此:
協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài)(即所有局部狀態(tài)的一個(gè)特定組合),每次過程重入時(shí),就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài),換種說法:進(jìn)入上一次離開時(shí)所處邏輯流的位置。
在并發(fā)編程中,協(xié)程與線程類似,每個(gè)協(xié)程表示一個(gè)執(zhí)行單元,有自己的本地?cái)?shù)據(jù),與其它協(xié)程共享全局?jǐn)?shù)據(jù)和其它資源。
why:
目前主流語言基本上都選擇了多線程作為并發(fā)設(shè)施,與線程相關(guān)的概念是搶占式多任務(wù)(Preemptive multitasking),而與協(xié)程相關(guān)的是協(xié)作式多任務(wù)。
不管是進(jìn)程還是線程,每次阻塞、切換都需要陷入系統(tǒng)調(diào)用(system call),先讓CPU跑操作系統(tǒng)的調(diào)度程序,然后再由調(diào)度程序決定該跑哪一個(gè)進(jìn)程(線程)。而且由于搶占式調(diào)度執(zhí)行順序無法確定的特點(diǎn),使用線程時(shí)需要非常小心地處理同步問題,而協(xié)程完全不存在這個(gè)問題(事件驅(qū)動(dòng)和異步程序也有同樣的優(yōu)點(diǎn))。
因?yàn)閰f(xié)程是用戶自己來編寫調(diào)度邏輯的,對(duì)CPU來說,協(xié)程其實(shí)是單線程,所以CPU不用去考慮怎么調(diào)度、切換上下文,這就省去了CPU的切換開銷,所以協(xié)程在一定程度上又好于多線程。
how:
python里面怎么使用協(xié)程?答案是使用gevent,使用方法:看這里
使用協(xié)程,可以不受線程開銷的限制,我嘗試過一次把20W條url放在單進(jìn)程的協(xié)程里執(zhí)行,完全沒問題。
所以最推薦的方法,是多進(jìn)程+協(xié)程(可以看作是每個(gè)進(jìn)程里都是單線程,而這個(gè)單線程是協(xié)程化的)
多進(jìn)程+協(xié)程下,避開了CPU切換的開銷,又能把多個(gè)CPU充分利用起來,這種方式對(duì)于數(shù)據(jù)量較大的爬蟲還有文件讀寫之類的效率提升是巨大的。
為了管理協(xié)程和I/O的回調(diào)函數(shù),asyncio庫的事件循環(huán)也能基于定時(shí)的方式調(diào)用普通的函數(shù),使用call_soon()函數(shù),例子如下:
import?asyncio??
import?functools??
def?callback(arg,?*,?kwarg='default'):??
print('callback?invoked?with?{}?and?{}'.format(arg,?kwarg))??
async?def?main(loop):??
print('registering?callbacks')??
loop.call_soon(callback,?1)??
wrapped?=?functools.partial(callback,?kwarg='not?default')??
loop.call_soon(wrapped,?2)??
await?asyncio.sleep(0.1)??
event_loop?=?asyncio.get_event_loop()??
try:??
print('entering?event?loop')??
event_loop.run_until_complete(main(event_loop))??
finally:??
print('closing?event?loop')??
event_loop.close()
結(jié)果輸出如下:
entering event loop
registering callbacks
callback invoked with 1 and default
callback invoked with 2 and not default
closing event loop
當(dāng)前標(biāo)題:python協(xié)程函數(shù)切換 python協(xié)程返回值
本文地址:http://chinadenli.net/article18/doddegp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供自適應(yīng)網(wǎng)站、全網(wǎng)營(yíng)銷推廣、動(dòng)態(tài)網(wǎng)站、網(wǎng)頁設(shè)計(jì)公司、小程序開發(fā)、營(yíng)銷型網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)