協程

1. 協程介紹

2. 協程簡單實現

3.協程-greenlet版

4.協程-gevent版

 

 

1. 協程介紹

什麼是協程?

協程,又稱微線程,纖程。英文爲Coroutine。python

協程其實能夠認爲是比線程更小的執行單元。 爲啥說他是一個執行單元,由於他自帶CPU上下文。這樣只要在合適的時機, 咱們能夠把一個協程切換到另外一個協程。只要這個過程當中保存或恢復 CPU上下文,那麼程序仍是能夠運行的。程序員

通俗的理解:在一個線程中的某個函數,能夠在任何地方保存當前函數的一些臨時變量等信息,而後切換到另一個函數中執行,注意不是經過調用函數的方式作到的,而且切換的次數以及何時再切換到原來的函數都由開發者本身肯定。算法

協程和線程差別

這個過程看起來比線程差很少,其實否則。線程切換從系統層面來看遠不止保存和恢復CPU上下文這麼簡單。操做系統爲了程序運行的高效性,每一個線程都有本身緩存Cache等數據,操做系統還會幫你作這些數據的恢復操做,因此線程的切換很是耗性能。可是協程的切換隻是單純的操做CPU的上下文,因此一秒鐘切換個上百萬次系統都抗的住。緩存

協程的問題

可是協程有一個問題,就是系統並不感知,因此操做系統不會幫你作切換。那麼誰來幫你作切換?讓須要執行的協程更多地得到CPU時間纔是問題的關鍵。網絡

例子

目前的協程框架通常都是設計成 1:N 模式。所謂 1:N 就是一個線程做爲一個容器裏面放置多個協程。那麼誰來適時的切換這些協程?答案是由協程本身主動讓出CPU,也就是每一個協程池裏面有一個調度器,這個調度器是被動調度的,意思就是他不會主動調度。並且當一個協程發現本身執行不下去了(好比異步等待網絡的數據回來,可是當前尚未數據到),這個時候就能夠由這個協程通知調度器,這個時候執行到調度器的代碼,調度器根據事先設計好的調度算法找到當前最須要CPU的協程。切換這個協程的CPU上下文把CPU的運行權交個這個協程,直到這個協程出現執行不下去須要等等的狀況,或者它調用主動讓出CPU的API之類,觸發下一次調度。併發

那麼這個實現有沒有問題?

實際上是有問題的,假設這個線程中有一個協程是CPU密集型的,但他沒有IO操做, 也就是本身不會主動觸發調度器調度的過程,那麼就會出現其餘協程得不到執行的狀況,因此這種狀況下須要程序員本身避免。這是一個問題,假設業務開發的人員並不懂這個原理的話就可能會出現問題。框架

協程的好處

在IO密集型的程序中因爲IO操做遠遠慢於CPU的操做,因此每每須要CPU去等IO操做。同步IO下系統須要切換線程,讓操做系統能夠在IO過程當中執行其餘的東西。這樣雖然代碼是符合人類的思惟習慣,可是因爲大量的線程切換帶來了大量的性能的浪費,尤爲是IO密集型的程序。異步

因此人們發明了異步IO,就是當數據到達的時候觸發個人回調,來減小線程切換帶來性能損失。可是這樣的壞處也是很大的,主要的壞處就是操做被 「分片」 了,代碼寫的不是 「一鼓作氣」 這種,而是每次來段數據就要判斷數據夠不夠處理,夠處理就處理吧,不夠處理就再等等吧。這樣代碼的可讀性很低,其實也不符合人類的習慣。函數

可是協程能夠很好解決這個問題。好比把一個IO操做寫成一個協程,當觸發IO操做的時候就自動讓出CPU給其餘協程,要知道協程的切換很輕的。協程經過這種對異步IO的封裝既保留了性能也保證了代碼的容易編寫和可讀性。在高IO密集型的程序下很好,可是高CPU密集型的程序下沒啥好處。性能

 

2. 協程簡單實現

 1 import time 
 2 
 3 
 4 def a():
 5     while True:
 6         print("---A---")
 7         yield 
 8         time.sleep(0.5)
 9         
10 def b(c):
11     while True:
12         print("---B---")
13         next(c)
14         time.sleep(0.5)
15         
16 
17 if __name__ == "__main__":
18     one = a()  # 返回迭代器對象
19     b(one)  # 在b函數中不斷調用one.next()  

運行結果:

---B---
---A---
---B---
---A---
---B---
---A---
---B---
---A---
.......

 

3. 協程-greenlet版

爲了更好使用協程來完成多任務,python中的greenlet模塊對其封裝,從而使得切換任務變的更加簡單。

安裝greenlet模塊:pip install greenlet

示例:

 1 from greenlet import greenlet
 2 import time
 3 
 4 
 5 def test1():
 6     while True:
 7         print("---A---")
 8         gr2.switch()
 9         time.sleep(0.5)
10         
11 def test2():
12     while True:
13         print("---B---")
14         gr1.switch()
15         time.sleep(0.5)
16 
17 if __name__ == "__main__":        
18     gr1 = greenlet(test1)
19     gr2 = greenlet(test2)
20     gr1.switch()

運行結果:

---B---
---A---
---B---
---A---
---B---
---A---
---B---
---A---
.......

 

4. 協程-gevent版

greenlet已經實現了協程,可是這個還得人工切換,是否是以爲太麻煩了,不要捉急,python還有一個比greenlet更強大的而且可以自動切換任務的模塊gevent。

其原理是當一個greenlet遇到IO(指的是input、output即輸入輸出,好比網絡、文件操做等)操做時,像訪問網絡,就自動切換到其餘的greenlet,等到IO操做完成,再在適當的時候切換回來繼續執行。

因爲IO操做很是耗時,常常使程序處於等待狀態,有了gevent爲咱們自動切換協程,就保證總有greenlet在運行,而不是等待IO。

gevent的使用

python3安裝gevent:python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gevent

示例:

 1 import gevent
 2 
 3 
 4 def f(n):
 5     for i in range(n):
 6         print(gevent.getcurrent(), i)
 7   
 8   
 9 g1 = gevent.spawn(f, 5)
10 g2 = gevent.spawn(f, 5)
11 g3 = gevent.spawn(f, 5)
12 g1.join()
13 g2.join()
14 g3.join()

 運行效果:3個greenlet依次運行,而不是交替運行

<Greenlet at 0x3b01bd8: f(5)> 0
<Greenlet at 0x3b01bd8: f(5)> 1
<Greenlet at 0x3b01bd8: f(5)> 2
<Greenlet at 0x3b01bd8: f(5)> 3
<Greenlet at 0x3b01bd8: f(5)> 4
<Greenlet at 0x3b01ce8: f(5)> 0
<Greenlet at 0x3b01ce8: f(5)> 1
<Greenlet at 0x3b01ce8: f(5)> 2
<Greenlet at 0x3b01ce8: f(5)> 3
<Greenlet at 0x3b01ce8: f(5)> 4
<Greenlet at 0x3b01d70: f(5)> 0
<Greenlet at 0x3b01d70: f(5)> 1
<Greenlet at 0x3b01d70: f(5)> 2
<Greenlet at 0x3b01d70: f(5)> 3
<Greenlet at 0x3b01d70: f(5)> 4

gevent切換執行

 1 import gevent
 2 
 3 
 4 def f(n):
 5     for i in range(n):
 6         print(gevent.getcurrent(), i)
 7         # 用來模擬一個耗時操做,注意不是time模塊的sleep
 8         # 當有耗時操做時,gevent就會自動切換去執行空閒的協程
 9         gevent.sleep(1)
10   
11   
12 g1 = gevent.spawn(f, 5)
13 g2 = gevent.spawn(f, 5)
14 g3 = gevent.spawn(f, 5)
15 g1.join()
16 g2.join()
17 g3.join()

 

運行效果:3個greenlet交替運行

<Greenlet at 0x33f1bd8: f(5)> 0
<Greenlet at 0x33f1ce8: f(5)> 0
<Greenlet at 0x33f1d70: f(5)> 0
<Greenlet at 0x33f1bd8: f(5)> 1
<Greenlet at 0x33f1ce8: f(5)> 1
<Greenlet at 0x33f1d70: f(5)> 1
<Greenlet at 0x33f1bd8: f(5)> 2
<Greenlet at 0x33f1ce8: f(5)> 2
<Greenlet at 0x33f1d70: f(5)> 2
<Greenlet at 0x33f1bd8: f(5)> 3
<Greenlet at 0x33f1ce8: f(5)> 3
<Greenlet at 0x33f1d70: f(5)> 3
<Greenlet at 0x33f1bd8: f(5)> 4
<Greenlet at 0x33f1ce8: f(5)> 4
<Greenlet at 0x33f1d70: f(5)> 4

gevent併發下載器

固然,實際代碼裏,咱們不會用gevent.sleep()去切換協程,而是在執行到IO操做時,gevent自動切換。代碼以下:

 1 from gevent import monkey
 2 import gevent
 3 import urllib.request
 4 
 5 # 有IO操做時須要這一句
 6 monkey.patch_all()
 7 
 8 def myDownload(url):
 9     print("get: %s" % url)
10     # 請求站點,得到一個HTTPResponse對象
11     response = urllib.request.urlopen(url)
12     data = response.read()
13     print("%d bytes received from: %s" % (len(data), url))
14     
15 gevent.joinall([
16     gevent.spawn(myDownload, "http://www.baidu.com"),
17     gevent.spawn(myDownload, "http://www.163.com"),
18     gevent.spawn(myDownload, "http://www.cnblogs.com"),
19 ])

運行效果:收到數據的前後順序不必定與發送順序相同,這也就體現出了異步,即不肯定何時會收到數據,順序不必定。

get: http://www.baidu.com
get: http://www.163.com
get: http://www.cnblogs.com
499227 bytes received from: http://www.163.com
199947 bytes received from: http://www.baidu.com
48149 bytes received from: http://www.cnblogs.com
相關文章
相關標籤/搜索