1. 程式人生 > >【併發程式設計】IO模型

【併發程式設計】IO模型

 

一、要點回顧

為了更好地瞭解IO模型,我們需要先回顧下幾個概念:同步、非同步、阻塞、非阻塞

同步:

一個程序在執行某個任務時,另外一個程序必須等待其執行完畢,才能繼續執行。就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回。按照這個定義,其實絕大多數函式都是同步呼叫。但是一般而言,我們在說同步、非同步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。

非同步:

一個程序在執行某個任務時,其他程序不必等待其執行完畢就能開始執行。當一個非同步功能呼叫發出後,呼叫者不能立刻得到結果。當該非同步功能完成後,通過狀態、通知或回撥來通知呼叫者。如果非同步功能用狀態來通知。那麼呼叫者就需要每隔一定時間檢查一次,效率就很低(有些初學多執行緒程式設計的人,總喜歡用一個迴圈去檢查某個變數的值,這其實是一 種很嚴重的錯誤)。如果是使用通知的方式,效率則很高,因為非同步功能幾乎不需要做額外的操作。至於回撥函式,其實和通知沒太多區別。

阻塞:

阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起(如遇到io操作)。函式只有在得到結果之後才會將阻塞的執行緒啟用。有人也許會把阻塞呼叫和同步呼叫等同起來,實際上他是不同的。對於同步呼叫來說,很多時候當前執行緒還是啟用的,只是從邏輯上當前函式沒有返回而已。

非阻塞:

不能立刻得到結果之前也會立刻返回,同時該函式不會阻塞當前執行緒。

總結:

  1. 同步與非同步針對的是函式/任務的呼叫方式:同步就是當一個程序發起一個函式(任務)呼叫的時候,一直等到函式(任務)完成,而程序繼續處於啟用狀態。而非同步情況下是當一個程序發起一個函式(任務)呼叫的時候,不會等函式返回,而是繼續往下執行當,函式返回的時候通過狀態、通知、事件等方式通知程序任務完成;
  2. 阻塞與非阻塞針對的是程序或執行緒:阻塞是當請求不能滿足的時候就將程序掛起,而非阻塞則不會阻塞當前程序。

二、阻塞IO

阻塞IO模型流程如下:

阻塞IO的特點就是在IO執行的兩個階段(等待資料和拷貝資料)都阻塞了。

實際上,除非特別指定,幾乎所有的IO介面 ( 包括socket介面 ) 都是阻塞型的。這給網路程式設計帶來了一個很大的問題,如在呼叫recv(1024)的同時,執行緒將被阻塞,在此期間,執行緒將無法執行任何運算或響應任何的網路請求。

 1 from socket import *
 2 server = socket(AF_INET,SOCK_STREAM)
3 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 4 server.bind(('127.0.0.1',8080)) 5 server.listen(5) 6 print('server start...') 7 while True: 8 conn,addr = server.accept() #IO操作 在這accept的時候不能幹recv的活 9 print(addr) 10 while True: 11 try: 12 data = conn.recv(1024) #IO操作 13 conn.send(data.upper()) 14 except Exception: 15 break 16 conn.close() 17 server.close()
服務端
 1 from socket import *
 2 client  = socket(AF_INET,SOCK_STREAM)
 3 client.connect(('127.0.0.1',8080))
 4 while True:
 5     cmd = input('>>:').strip()
 6     if not cmd:continue
 7     client.send(cmd.encode('utf-8'))
 8     data = client.recv(1024)
 9     print('接受的是:%s'%data.decode('utf-8'))
10 client.close()
客戶端

一旦阻塞了就在那卡著直到等到資料已經到了作業系統,作業系統再從核心拷貝給應用程式阻塞IO在那兩個階段全都阻塞住了。這樣要想實現併發,可以使用多執行緒,多程序或執行緒池等方式。而多執行緒一旦連線過多,執行緒過多,會極大消耗系統資源。

三、非阻塞IO

recvfrom發起系統呼叫後, 如果資料未準備好, 核心也會立即返回, 這樣使用者程式可以不用阻塞繼續執行後面的操作,但需要迴圈向核心發出系統呼叫來查詢資料是否準備好,一旦資料準備好,recvfrom便會發起系統呼叫從核心空間拷貝資料,拷貝資料的過程是阻塞的。

 1 from socket import *
 2 
 3 server = socket(AF_INET, SOCK_STREAM)
 4 server.bind(('127.0.0.1',8099))
 5 server.listen(5)
 6 server.setblocking(False)
 7 
 8 
 9 rlist=[]
10 wlist=[]
11 while True:
12     try:
13         conn, addr = server.accept()
14         rlist.append(conn)
15         print(rlist)
16 
17     except BlockingIOError:
18         del_rlist=[]
19         for sock in rlist:
20             try:
21                 data=sock.recv(1024)
22                 if not data:
23                     del_rlist.append(sock)
24                 wlist.append((sock,data.upper()))
25             except BlockingIOError:
26                 continue
27             except Exception:
28                 sock.close()
29                 del_rlist.append(sock)
30 
31         del_wlist=[]
32         for item in wlist:
33             try:
34                 sock = item[0]
35                 data = item[1]
36                 sock.send(data)
37                 del_wlist.append(item)
38             except BlockingIOError:
39                 pass
40 
41         for item in del_wlist:
42             wlist.remove(item)
43 
44 
45         for sock in del_rlist:
46             rlist.remove(sock)
47 
48 server.close()
服務端
 1 from socket import *
 2 c=socket(AF_INET,SOCK_STREAM)
 3 c.connect(('127.0.0.1',8080))
 4 
 5 while True:
 6     msg=input('>>: ')
 7     if not msg:continue
 8     c.send(msg.encode('utf-8'))
 9     data=c.recv(1024)
10     print(data.decode('utf-8'))
客戶端

這樣也存在一個缺點:大量進行系統呼叫, 會極大消耗cpu資源; 同時由於查詢間隔, 將不能及時的獲取資料。

四、多路複用IO

當用戶程序呼叫了select,那麼整個程序會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。
這個圖和blocking IO的圖其實並沒有太大的不同,事實上還更差一些。因為這裡需要使用兩個系統呼叫(select和recvfrom),而blocking IO只調用了一個系統呼叫(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

   強調:

    1. 如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server效能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。

    2. 在多路複用模型中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。

    結論: select的優勢在於可以處理多個連線,不適用於單個連線

 1 from socket import *
 2 import select
 3 server = socket(AF_INET,SOCK_STREAM)
 4 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
 5 server.bind(('127.0.0.1',8081))
 6 server.setblocking(False) #設定socket的套接字為非阻塞的
 7 server.listen(5)
 8 print('start running....')
 9 read_l = [server,]  #因為不只就那麼一個列表要檢測。所以不要在引數裡面定死了
10 while True:
11     r_l,w_l,x_l = select.select(read_l,[],[])  #select()方法有四個引數
12     print(r_l)  #一開始服務端執行的時候,就等著,當你客戶端一連結的時候,他就
13                 # 檢測到有資料了(檢測那個資料準備好了)
14     for obj in r_l:
15         if obj == server:
16             conn,addr = obj.accept()  #accept要經歷兩個階段,但是程式如果走到這一步,那肯定是資料準備好了
17                          #當資料已經準備好的時候,accept就只經歷一個copy資料的階段了
18             # print(addr)
19             read_l.append(conn)  #在監聽一下conn套接字(這時候已經監聽了兩個了:分別是accept,conn)
20         else:
21             data = obj.recv(1024)  # 此時的obj=conn
22             obj.send(data.upper())
23 #         obj.close()
24 # server.close()
服務端
 1 from socket import *
 2 import select
 3 client = socket(AF_INET,SOCK_STREAM)
 4 client.connect(('127.0.0.1',8081))
 5 while True:
 6     cmd = input('>>:')
 7     client.send(cmd.encode('utf-8'))
 8     data = client.recv(1024)
 9     print('接收的是:%s'%data.decode('utf-8'))
10 client.close()
客戶端

優點:

相比其他模型,使用select() 的事件驅動模型只用單執行緒(程序)執行,佔用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務。如果試圖建立一個簡單的事件驅動的伺服器程式,這個模型有一定的參考價值。select模組用select方法檢測那個套接字準備好了,也就是收沒收到資料(而我們的非阻塞IO你不知道那個套接字準備好了,那麼用select模組就能解決這個問題)。select還可以檢測多個套接字,所以select比非阻塞IO的效率高。

缺點:

首先select()介面並不是實現“事件驅動”的最好選擇。因為當需要探測的控制代碼值較大時,select()介面本身需要消耗大量時間去輪詢各個控制代碼。很多作業系統提供了更為高效的介面,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
如果需要實現更高效的伺服器程式,類似epoll這樣的介面更被推薦。遺憾的是不同的作業系統特供的epoll介面有很大差異,所以使用類似於epoll的介面實現具有較好跨平臺能力的伺服器會比較困難。其次,該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。

五、非同步IO

未完待續……