協程,又稱微線程,纖程。英文爲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密集型的程序下沒啥好處。性能
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---
.......
爲了更好使用協程來完成多任務,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---
.......
greenlet已經實現了協程,可是這個還得人工切換,是否是以爲太麻煩了,不要捉急,python還有一個比greenlet更強大的而且可以自動切換任務的模塊gevent。
其原理是當一個greenlet遇到IO(指的是input、output即輸入輸出,好比網絡、文件操做等)操做時,像訪問網絡,就自動切換到其餘的greenlet,等到IO操做完成,再在適當的時候切換回來繼續執行。
因爲IO操做很是耗時,常常使程序處於等待狀態,有了gevent爲咱們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
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
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.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