1. 程式人生 > >線程、進程與協程2

線程、進程與協程2

部分 tin 另一個 locking 寄存器 out 可能 ket .so

一、協程

什麽是協程?

協程,又名微線程,纖程,英文名為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