線程、進程與協程2
一、協程
什麽是協程?
協程,又名微線程,纖程,英文名為Coroutine。
協程是一種用戶態的輕量級線程。
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。
因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。
CPU只認識線程,不認識協程。線程的寄存器在CPU上,協程的寄存器不在CPU上。
yield就是在單線程下通過yield切換的協程。
協程的好處?
1.使用協程,無需線程上下文切換的開銷。
用協程在單線程下實現並發效果,實際上是在單線程下實現函數之間的切換,不涉及CPU的切換。
2.無需原子操作鎖定及同步的開銷。
“原子操作”atomic operation是不需要synchronized,所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任務的context switch(上下文切換——切換到另一個線程)。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序是不可以被打亂,或者切割掉只執行部分。視作整體是原子性的核心。
3.方便切換控制流,簡化編程模型
4.高並發+高擴展性+低成本:一個CPU支持上萬的協程都不是問題。所以很適合用於高並發處理。
協程的缺點?
1.無法利用多核資源。
協程的本質是個單線程,它不能同時將單個CPU的多個核用上,協程需要和進程配合才能運行在多CPU上。
當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是CPU密集型應用。
2.進行阻塞Blocking操作(如IO時)會阻塞掉整個程序
協程的標準定義,標準形態
符合標準的協程必須滿足以下4點的功能:
1.必須在只有一個單線程裏實現並發
2.修改共享數據不需要加鎖
3.用戶程序裏自己保存多個控制流的上下文棧
4.一個協程遇到IO操作自動切換到其他協程
greenlet 和 Gevent
Python通過yield提供了對協程的基本支持,但是不完全。而第三方的gevent為Python提供了比較完善的協程支持。
greenlet是一個用C實現的協程模塊,相比與python自帶的yield,它可以使你在任意函數之間隨意切換,而不需把這個函數先聲明為generator.
gevent是一個第三方庫,可以輕松通過gevent實現並發同步或異步編程,在gevent中用到的主要模式是greenlet,它是以C擴展模塊形式接入Python的輕量級協程。
greenlet全部運行在主程序操作系統進程的內部,但它們被協作式地調度。
gevent通過greenlet實現協程,其基本思想是:
當一個greenlet遇到IO操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
greenlet實例:
1 from greenlet import greenlet 2 def test1(): 3 print(12) 4 gr2.switch() #step3:切換到gr2,運行test2() print(56) 5 print(34) 6 gr2.switch() #step5:切換到gr2,運行test2() print(78) 7 8 def test2(): 9 print(56) 10 gr1.switch() #step4:切換到gr1,運行test1() print(34) 11 print(78) 12 13 gr1=greenlet(test1) #step1:生成greenlet實例 14 gr2=greenlet(test2) 15 gr1.switch() #step2:切換到gr1,運行test1() print(12)View Code
返回:
greenlet通過switch()手動切換函數,在單線程下實現協程的效果。
Gevent實例:
1 import gevent 2 def fun1(): 3 print(1) 4 gevent.sleep(1) 5 print(2) 6 7 def fun2(): 8 print(3) 9 gevent.sleep(1) 10 print(4) 11 12 def fun3(): 13 print(5) 14 gevent.sleep(4) 15 print(6) 16 17 gevent.joinall([ 18 gevent.spawn(fun1), 19 gevent.spawn(fun2), 20 gevent.spawn(fun3) 21 ])View Code
返回:
註意:gevent中的方法:
gevent.joinall(greenlets,timeout=None,raise_error=False,count=None)
gevent.spawn(*args,**kwargs)
實例:遇到IO阻塞時自動切換任務:
由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啟動時通過monkey patch完成:
1 import gevent 2 from urllib.request import urlopen #由於gevent無法監測urllib中的IO操作,所以要加上monkey.patch_all(),做上標記後後實現IO的自動切換。 3 import re 4 def f(url): 5 print(url) 6 resp=urlopen(url) #生成一個urlopen實例 7 data=resp.read() #讀取urlopen實例化對象的內容 8 print(len(data),url) 9 10 11 gevent.joinall([ 12 gevent.spawn(f,‘http://sh.gsxt.gov.cn/notice/notice/view?uuid=tJrWRfxkMtxQqih9h7lwTujrR0nXE6pM&tab=01‘), 13 gevent.spawn(f,‘https://www.tianyancha.com/company/22823‘), 14 gevent.spawn(f,‘https://www.tianyancha.com/login?from=https%3A%2F%2Fwww.tianyancha.com%2Fcompany%2F24489290‘) 15 ])View Code
註意:在gevent中無法判斷直接引用是不清楚urllib中是否有IO切換的,只要在腳本最前面加上“from gevent import monkey;monkey.patch_all()”就可以實現在IO操作前進行標記。
實例:通過gevent實現單線程下的多socket並發
server端:
1 import sys 2 import socket 3 import time 4 import gevent 5 6 from gevent import monkey,socket 7 monkey.patch_all() 8 9 def server(port): 10 s=socket.socket() 11 s.bind((‘0.0.0.0‘,port)) 12 s.listen(500) 13 while True: 14 cli,addr=s.accept() 15 gevent.spawn(handle_request,cli) 16 17 def handle_request(conn): 18 try: 19 while True: 20 data=conn.recv(1024) 21 print(‘recv:‘,data) 22 conn.send(data) 23 if not data: 24 conn.shutdown(socket.SHUT_WR) 25 26 except Exception as ex: 27 print(ex) 28 finally: 29 conn.close() 30 31 if __name__==‘__main__‘: 32 server(8001)View Code
client端:
1 import socket 2 3 HOST = ‘localhost‘ # The remote host 4 PORT = 8001 # The same port as used by the server 5 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 s.connect((HOST, PORT)) 7 while True: 8 msg = bytes(input(">>:"), encoding="utf-8") 9 s.sendall(msg) 10 data = s.recv(1024) 11 print(‘Received‘, data) 12 13 s.close()View Code
這個實例通過協程實現了一個大規模的多並發的socket server。
論事件驅動與異步IO
通常,我們寫服務器處理模型的程序時,有以下幾種模型:
(1) 每收到一個請求,創建一個新的進程來處理該請求; server=socketserver.ForkingTCPServer()
(2) 每收到一個請求,創建一個新的線程來處理該請求; server=socketserver.ThreadingTCPServer()
(3) 每收到一個請求,放入一個事件列表,讓主進程通過非阻塞I/O方式(協程)來處理請求
上面的方法的優劣:
第一種方法,由於創建新的進程的開銷比較大,所以會導致服務器性能比較差,但實現比較簡單。
第二種方法,由於要涉及到線程的同步,有可能會面臨死鎖等問題。
第三種方法,在寫應用程序代碼時,邏輯比前面兩種都復雜。
綜合考慮,一般普遍認為第三種方式是大多數網絡服務器采用的方式。
異步IO
線程、進程與協程2