Python學(xué)習(xí)之路-多任務(wù):協(xié)程
簡介
協(xié)程,又稱微線程,纖程。英文名Coroutine。協(xié)程是python個中另外一種實現(xiàn)多任務(wù)的方式,只不過比線程更小占用更小執(zhí)行單元(理解為需要的資源)。 為啥說它是一個執(zhí)行單元,因為它自帶CPU上下文。這樣只要在合適的時機(jī), 我們可以把一個協(xié)程 切換到另一個協(xié)程。 只要這個過程中保存或恢復(fù) CPU上下文那么程序還是可以運(yùn)行的。
通俗的理解:在一個線程中的某個函數(shù),可以在任何地方保存當(dāng)前函數(shù)的一些臨時變量等信息,然后切換到另外一個函數(shù)中執(zhí)行,注意不是通過調(diào)用函數(shù)的方式做到的,并且切換的次數(shù)以及什么時候再切換到原來的函數(shù)都由開發(fā)者自己確定
協(xié)程和線程差異
在實現(xiàn)多任務(wù)時, 線程切換從系統(tǒng)層面遠(yuǎn)不止保存和恢復(fù) CPU上下文這么簡單。 操作系統(tǒng)為了程序運(yùn)行的高效性每個線程都有自己緩存Cache等等數(shù)據(jù),操作系統(tǒng)還會幫你做這些數(shù)據(jù)的恢復(fù)操作。 所以線程的切換非常耗性能。但是協(xié)程的切換只是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統(tǒng)都抗的住。
迭代
迭代是訪問集合元素的一種方式。使用for…in…的循環(huán)語法從其中依次拿到數(shù)據(jù)進(jìn)行使用,我們把這樣的過程稱為遍歷,也叫迭代。
迭代器
迭代器是一個可以記住遍歷的位置的對象。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結(jié)束。迭代器只能往前不會后退。
可迭代對象
把可以通過for…in…這類語句迭代讀取一條數(shù)據(jù)供我們使用的對象稱之為可迭代對象(Iterable)??梢允褂?isinstance() 判斷一個對象是否是可迭代對象 Iterable 對象。
可迭代對象的本質(zhì)
我們分析對可迭代對象進(jìn)行迭代使用的過程,發(fā)現(xiàn)每迭代一次(即在for…in…中每循環(huán)一次)都會返回對象中的下一條數(shù)據(jù),一直向后讀取數(shù)據(jù)直到迭代了所有數(shù)據(jù)后結(jié)束。那么,在這個過程中就應(yīng)該有一個“人”去記錄每次訪問到了第幾條數(shù)據(jù),以便每次迭代都可以返回下一條數(shù)據(jù)。我們把這個能幫助我們進(jìn)行數(shù)據(jù)迭代的“人”稱為迭代器(Iterator)。
可迭代對象的本質(zhì)就是可以向我們提供一個這樣的中間“人”即迭代器幫助我們對其進(jìn)行迭代遍歷使用。
可迭代對象通過__iter__
方法向我們提供一個迭代器,我們在迭代一個可迭代對象的時候,實際上就是先獲取該對象提供的一個迭代器,然后通過這個迭代器來依次獲取對象中的每一個數(shù)據(jù).
那么也就是說,一個具備了__iter__
方法的對象,就是一個可迭代對象。
{{< admonition warning “注意” true >}}
可以通過iter()函數(shù)獲取這些可迭代對象的迭代器(iter()
函數(shù)實際上就是調(diào)用了可迭代對象的__iter__
方法),然后我們可以對獲取到的迭代器不斷使用next()
函數(shù)來獲取下一條數(shù)據(jù)(使用next()
函數(shù)的時候,調(diào)用的就是迭代器對象的__next__
方法)。當(dāng)我們已經(jīng)迭代完最后一個數(shù)據(jù)之后,再次調(diào)用next()函數(shù)會拋出StopIteration的異常,來告訴我們所有數(shù)據(jù)都已迭代完成,不用再執(zhí)行next()函數(shù)了。
{{< /admonition >}}
for…in…循環(huán)的本質(zhì)
for item in Iterable 循環(huán)的本質(zhì)就是先通過iter()函數(shù)獲取可迭代對象Iterable的迭代器,然后對獲取到的迭代器不斷調(diào)用next()方法來獲取下一個值并將其賦值給item,當(dāng)遇到StopIteration的異常后循環(huán)結(jié)束。
生成器
簡介
生成器是一類特殊的迭代器。利用迭代器,我們可以在每次迭代獲取數(shù)據(jù)(通過next()方法)時按照特定的規(guī)律進(jìn)行生成。但是我們在實現(xiàn)一個迭代器時,關(guān)于當(dāng)前迭代到的狀態(tài)需要我們自己記錄,進(jìn)而才能根據(jù)當(dāng)前狀態(tài)生成下一個數(shù)據(jù)。為了達(dá)到記錄當(dāng)前狀態(tài),并配合next()函數(shù)進(jìn)行迭代使用,我們可以采用更簡便的語法,即生成器(generator)
創(chuàng)建生成器方法
-
只要把一個列表生成式的 [ ] 改成 ( ),如下
In [1]: A = [ x*2 for x in range(5)] In [2]: A Out[2]: [0, 2, 4, 6, 8] In [3]: B = ( x*2 for x in range(5)) In [4]: B Out[4]: <generator object <genexpr> at 0x7f626c132db0>
創(chuàng)建 L 和 G 的區(qū)別僅在于最外層的 [ ] 和 ( ) , L 是一個列表,而 G 是一個生成器。我們可以直接打印出列表L的每一個元素,而對于生成器G,我們可以按照迭代器的使用方法來使用,即可以通過next()函數(shù)、for循環(huán)、list()等方法使用。
In [5]: next(G) Out[5]: 0 In [6]: next(G) Out[6]: 2 In [7]: next(G) Out[7]: 4 In [8]: next(G) Out[8]: 6 In [9]: next(G) Out[9]: 8 In [10]: next(G) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-24-380e167d6934> in <module>() ----> 1 next(G) StopIteration: In [11]: In [12]: G = ( x*2 for x in range(5)) In [13]: for x in G: ....: print(x) ....: 0 2 4 6 8 In [14]:
-
將原本在迭代器
__next__
方法中實現(xiàn)的基本邏輯放到一個函數(shù)中來實現(xiàn),但是將每次迭代返回數(shù)值的return換成了yield,此時新定義的函數(shù)便不再是函數(shù),而是一個生成器了。簡單來說:只要在def中有yield關(guān)鍵字的 就稱為 生成器。用生成器實現(xiàn)斐波那契數(shù)列In [15]: def fib(n): ....: current = 0 ....: num1, num2 = 0, 1 ....: while current < n: ....: num = num1 ....: num1, num2 = num2, num1+num2 ....: current += 1 ....: yield num ....: return 'done' ....: In [16]: F = fib(5) In [17]: next(F) Out[17]: 1 In [18]: next(F) Out[18]: 1 In [19]: next(F) Out[19]: 2 In [20]: next(F) Out[20]: 3 In [21]: next(F) Out[21]: 5 In [22]: next(F) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-22-8c2b02b4361a> in <module>() ----> 1 next(F) StopIteration: done
此時按照調(diào)用函數(shù)的方式( 案例中為F = fib(5) )使用生成器就不再是執(zhí)行函數(shù)體了,而是會返回一個生成器對象( 案例中為F ),然后就可以按照使用迭代器的方式來使用生成器了。
In [23]: for n in fib(5): ....: print(n) ....: 1 1 2 3 5
但是用for循環(huán)調(diào)用generator時,發(fā)現(xiàn)拿不到generator的return語句的返回值。如果想要拿到返回值,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:
In [25]: g = fib(5) In [26]: while True: ....: try: ....: x = next(g) ....: print("value:%d"%x) ....: except StopIteration as e: ....: print("生成器返回值:%s"%e.value) ....: break ....: value:1 value:1 value:2 value:3 value:5 生成器返回值:done
使用send喚醒
我們除了可以使用next()函數(shù)來喚醒生成器繼續(xù)執(zhí)行外,還可以使用send()函數(shù)來喚醒執(zhí)行。使用send()函數(shù)的一個好處是可以在喚醒的同時向斷點處傳入一個附加數(shù)據(jù)。
例子:執(zhí)行到y(tǒng)ield時,gen函數(shù)作用暫時保存,返回i的值; temp接收下次c.send(“python”),send發(fā)送過來的值,c.next()等價c.send(None)
In [27]: def gen():
....: i = 0
....: while i<5:
....: temp = yield i
....: print(temp)
....: i+=1
....:
使用send
In [28]: f = gen()
In [29]: next(f)
Out[29]: 0
In [30]: f.send('haha')
haha
Out[30]: 1
In [31]: next(f)
None
Out[31]: 2
In [32]: f.send('haha')
haha
Out[32]: 3
使用next函數(shù)
In [33]: f = gen()
In [34]: next(f)
Out[34]: 0
In [35]: next(f)
None
Out[35]: 1
In [36]: next(f)
None
Out[36]: 2
In [37]: next(f)
None
Out[37]: 3
In [38]: next(f)
None
Out[38]: 4
In [39]: next(f)
None
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)
StopIteration:
使用__next__()
方法(不常使用)
In [40]: f = gen()
In [41]: f.__next__()
Out[41]: 0
In [42]: f.__next__()
None
Out[42]: 1
In [43]: f.__next__()
None
Out[43]: 2
In [44]: f.__next__()
None
Out[44]: 3
In [45]: f.__next__()
None
Out[45]: 4
In [46]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()
StopIteration:
簡單實現(xiàn)協(xié)程
import time
def work1():
while True:
print("----work1---")
yield
time.sleep(0.5)
def work2():
while True:
print("----work2---")
yield
time.sleep(0.5)
def main():
w1 = work1()
w2 = work2()
while True:
next(w1)
next(w2)
if __name__ == "__main__":
main()
運(yùn)行結(jié)果:
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...
greenlet
為了更好使用協(xié)程來完成多任務(wù),python中的greenlet模塊對其封裝,從而使得切換任務(wù)變的更加簡單
安裝
使用如下命令安裝greenlet模塊:
sudo pip3 install greenlet
簡單實現(xiàn)
from greenlet import greenlet
import time
def test1():
while True:
print "---A--"
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print "---B--"
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切換到gr1中運(yùn)行
gr1.switch()
運(yùn)行效果:
---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...
gevent
greenlet已經(jīng)實現(xiàn)了協(xié)程,但是這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個比greenlet更強(qiáng)大的并且能夠自動切換任務(wù)的模塊gevent
其原理是當(dāng)一個greenlet遇到IO(指的是input output 輸入輸出,比如網(wǎng)絡(luò)、文件操作等)操作時,比如訪問網(wǎng)絡(luò),就自動切換到其他的greenlet,等到IO操作完成,再在適當(dāng)?shù)臅r候切換回來繼續(xù)執(zhí)行。
由于IO操作非常耗時,經(jīng)常使程序處于等待狀態(tài),有了gevent為我們自動切換協(xié)程,就保證總有g(shù)reenlet在運(yùn)行,而不是等待IO
安裝
使用如下命令安裝gevent模塊:
pip3 install gevent
簡單實現(xiàn)
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
運(yùn)行結(jié)果
<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4
可以看到,3個greenlet是依次運(yùn)行而不是交替運(yùn)行
切換執(zhí)行
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
#用來模擬一個耗時操作,注意不是time模塊中的sleep
gevent.sleep(1)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
運(yùn)行結(jié)果
<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4
給程序打補(bǔ)丁
from gevent import monkey
import gevent
import random
import time
def coroutine_work(coroutine_name):
for i in range(10):
print(coroutine_name, i)
time.sleep(random.random())
gevent.joinall([
gevent.spawn(coroutine_work, "work1"),
gevent.spawn(coroutine_work, "work2")
])
運(yùn)行結(jié)果文章來源:http://www.zghlxwxcb.cn/news/detail-797584.html
work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
monkey
from gevent import monkey
import gevent
import random
import time
# 有耗時操作時需要
monkey.patch_all() # 將程序中用到的耗時操作的代碼,換為gevent中自己實現(xiàn)的模塊
def coroutine_work(coroutine_name):
for i in range(10):
print(coroutine_name, i)
time.sleep(random.random())
gevent.joinall([
gevent.spawn(coroutine_work, "work1"),
gevent.spawn(coroutine_work, "work2")
])
運(yùn)行結(jié)果文章來源地址http://www.zghlxwxcb.cn/news/detail-797584.html
work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9
進(jìn)程、線程、協(xié)程對比
- 進(jìn)程是資源分配的單位
- 線程是操作系統(tǒng)調(diào)度的單位
- 進(jìn)程切換需要的資源很最大,效率很低
- 線程切換需要的資源一般,效率一般(當(dāng)然了在不考慮GIL的情況下)
- 協(xié)程切換任務(wù)資源很小,效率高
- 多進(jìn)程、多線程根據(jù)cpu核數(shù)不一樣可能是并行的,但是協(xié)程是在一個線程中 所以是并發(fā)
到了這里,關(guān)于Python學(xué)習(xí)之路-多任務(wù):協(xié)程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!