一、協程定義
協程其實可以認為是比線程更小的執行單元。 為啥說他是一個執行單元,因為他自帶CPU上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。 只要這個過程中保存或恢復 CPU上下文那么程序還是可以運行的。
二、協程切換和線程切換對比
線程切換從系統層面遠不止保存和恢復 CPU上下文這么簡單。 操作系統為了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統還會幫你做這些數據的恢復操作。 所以線程的切換非常耗性能。但是協程的切換只是單純的操作CPU的上下文,所以一秒鍾切換個上百萬次系統都抗的住。
三、協程帶來的問題
協程有一個問題,就是系統並不感知,所以操作系統不會幫你做切換。 那么誰來幫你做切換?讓需要執行的協程更多的獲得CPU時間才是問題的關鍵。
舉個例子如下:
目前的協程框架一般都是設計成 1:N 模式。所謂 1:N 就是一個線程作為一個容器里面放置多個協程。 那么誰來適時的切換這些協程?答案是有協程自己主動讓出CPU,也就是每個協程池里面有一個調度器, 這個調度器是被動調度的。意思就是他不會主動調度。而且當一個協程發現自己執行不下去了(比如異步等待網絡的數據回來,但是當前還沒有數據到), 這個時候就可以由這個協程通知調度器,這個時候執行到調度器的代碼,調度器根據事先設計好的調度算法找到當前最需要CPU的協程。 切換這個協程的CPU上下文把CPU的運行權交個這個協程,直到這個協程出現執行不下去需要等等的情況,或者它調用主動讓出CPU的API之類,觸發下一次調度。
四、協程處理好處
在IO密集型的程序中由於IO操作遠遠慢於CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系統需要切換線程,讓操作系統可以在IO過程中執行其他的東西。 這樣雖然代碼是符合人類的思維習慣但是由於大量的線程切換帶來了大量的性能的浪費,尤其是IO密集型的程序。
(不開啟多協程情況下,程序是按順序執行的,上面的不執行完是不會執行下面的代碼。這樣的話如果有IO輸入操作,這個時候程序就會自動暫停等待用戶輸入,那么這個等待的時間就被浪費了)
所以人們發明了異步IO。就是當數據到達的時候觸發我的回調。來減少線程切換帶來性能損失。 但是這樣的壞處也是很大的,主要的壞處就是操作被 “分片” 了,代碼寫的不是 “一氣呵成” 這種。 而是每次來段數據就要判斷 數據夠不夠處理哇,夠處理就處理吧,不夠處理就在等等吧。
但是協程可以很好解決這個問題。比如 把一個IO操作 寫成一個協程。當觸發IO操作的時候就自動讓出CPU給其他協程。要知道協程的切換很輕的。 協程通過這種對異步IO的封裝 既保留了性能也保證了代碼的容易編寫和可讀性。在高IO密集型的程序下很好。但是高CPU密集型的程序下沒啥好處。
但是這樣的話程序運行的順序就會亂,不會按照程序代碼順序執行。很好理解嘛
五、猴子補丁有什么用?
先看一個例子
1 import gevent,time 2 3 def printf1(num): 4 for i in range(num): 5 print (i,gevent.getcurrent()) 6 time.sleep(1) 7 8 def printf2(num): 9 for i in range(num): 10 print (i,gevent.getcurrent()) 11 time.sleep(1) 12 13 def printf3(num): 14 for i in range(num): 15 print (i,gevent.getcurrent()) 16 time.sleep(1) 17 18 g1 = gevent.spawn(printf1,3) 19 g2 = gevent.spawn(printf2,2) 20 g3 = gevent.spawn(printf3,5) 21 g1.join() 22 g2.join() 23 g3.join() 24 25 '''輸出結果 26 0 <Greenlet at 0x27a15691598: printf1(3)> 27 1 <Greenlet at 0x27a15691598: printf1(3)> 28 2 <Greenlet at 0x27a15691598: printf1(3)> 29 0 <Greenlet at 0x27a156917b8: printf2(2)> 30 1 <Greenlet at 0x27a156917b8: printf2(2)> 31 0 <Greenlet at 0x27a156916a8: printf3(5)> 32 1 <Greenlet at 0x27a156916a8: printf3(5)> 33 2 <Greenlet at 0x27a156916a8: printf3(5)> 34 3 <Greenlet at 0x27a156916a8: printf3(5)> 35 4 <Greenlet at 0x27a156916a8: printf3(5)> 36 '''
我們會發現,我們用了協程,還有IO操作,但是為啥還是按順序來的。
再看一個例子
1 import gevent,time 2 3 def printf1(num): 4 for i in range(num): 5 print ("這是第",i,gevent.getcurrent()) 6 gevent.sleep(1) #就只改了一下這里 7 8 def printf2(num): 9 for i in range(num): 10 print ("這是第",i,gevent.getcurrent()) 11 gevent.sleep(1) #就只改了一下這里 12 13 def printf3(num): 14 for i in range(num): 15 print ("這是第",i,gevent.getcurrent()) 16 gevent.sleep(1) #就只改了一下這里 17 18 g1 = gevent.spawn(printf1,3) 19 g2 = gevent.spawn(printf2,3) 20 g3 = gevent.spawn(printf3,3) 21 g1.join() 22 g2.join() 23 g3.join() 24 25 '''輸出結果 26 這是第 0 <Greenlet at 0x1c0746f1598: printf1(3)> 27 這是第 0 <Greenlet at 0x1c0746f17b8: printf2(3)> 28 這是第 0 <Greenlet at 0x1c0746f16a8: printf3(3)> 29 這是第 1 <Greenlet at 0x1c0746f1598: printf1(3)> 30 這是第 1 <Greenlet at 0x1c0746f17b8: printf2(3)> 31 這是第 1 <Greenlet at 0x1c0746f16a8: printf3(3)> 32 這是第 2 <Greenlet at 0x1c0746f1598: printf1(3)> 33 這是第 2 <Greenlet at 0x1c0746f17b8: printf2(3)> 34 這是第 2 <Greenlet at 0x1c0746f16a8: printf3(3)> 35 '''
發現這才是我們想要的情況。我們發現用time.sleep(1)不行,用gevent.sleep(1)可以。我們知道其實他們的作用都是讓程序休眠1秒。為什么time.sleep(1)不行呢
再看一個例子
1 import gevent 2 3 from gevent import monkey 4 monkey.patch_all() 5 import time 6 def printf1(num): 7 for i in range(num): 8 print ("這是第",i,gevent.getcurrent()) 9 time.sleep(1) #就只改了一下這里 10 11 def printf2(num): 12 for i in range(num): 13 print ("這是第",i,gevent.getcurrent()) 14 time.sleep(1) #就只改了一下這里 15 16 def printf3(num): 17 for i in range(num): 18 print ("這是第",i,gevent.getcurrent()) 19 time.sleep(1) #就只改了一下這里 20 21 g1 = gevent.spawn(printf1,3) 22 g2 = gevent.spawn(printf2,3) 23 g3 = gevent.spawn(printf3,3) 24 g1.join() 25 g2.join() 26 g3.join() 27 28 '''輸出結果 29 這是第 0 <Greenlet at 0x1c0746f1598: printf1(3)> 30 這是第 0 <Greenlet at 0x1c0746f17b8: printf2(3)> 31 這是第 0 <Greenlet at 0x1c0746f16a8: printf3(3)> 32 這是第 1 <Greenlet at 0x1c0746f1598: printf1(3)> 33 這是第 1 <Greenlet at 0x1c0746f17b8: printf2(3)> 34 這是第 1 <Greenlet at 0x1c0746f16a8: printf3(3)> 35 這是第 2 <Greenlet at 0x1c0746f1598: printf1(3)> 36 這是第 2 <Greenlet at 0x1c0746f17b8: printf2(3)> 37 這是第 2 <Greenlet at 0x1c0746f16a8: printf3(3)> 38 '''
我們發現這次用的還是time.sleep(1),但是卻符合我們的心理預期了
# 協程任務之間的切換由程序代碼(gevent)完成,只有遇到協程模塊能識別到的IO操作的時候,程序才會進行協程切換,實現並發的效果
# 在這個patch_all后面的所有模塊中,發生的阻塞都會有gevent的效果
1 # gevent模塊 2 # 協程,在一個線程中來回的切換。這個切換過程不是操作系統做的,而是gevent做的 3 # 在這個patch_all后面的所有模塊中,發生的阻塞都會有gevent的效果 4 # #from gevent import monkey;monkey.patch_all() 5 # import time 6 # import gevent 7 # 8 # def eat(): 9 # print('eating start') 10 # time.sleep(1) # 發生IO阻塞,切換到play.因為有了mokey.patch_all所以這里等同於gevent.sleep(1),遇見IO就會切換 11 # print('eating end') 12 # 13 # def play(): 14 # print('playing start') 15 # time.sleep(1) # 發生IO阻塞,切換到eat 16 # print('playing end') 17 # 18 # if __name__ == '__main__': 19 # g1 = gevent.spawn(eat) 20 # g2 = gevent.spawn(play) 21 # g1.join() # 阻塞等待協程執行結束 22 # g2.join() #
參考博客:
https://blog.csdn.net/u014745194/article/details/71657575
https://blog.csdn.net/weixin_34310369/article/details/93753220