【併發程式設計】IO模型
一、要點回顧
為了更好地瞭解IO模型,我們需要先回顧下幾個概念:同步、非同步、阻塞、非阻塞
同步:
一個程序在執行某個任務時,另外一個程序必須等待其執行完畢,才能繼續執行。就是在發出一個功能呼叫時,在沒有得到結果之前,該呼叫就不會返回。按照這個定義,其實絕大多數函式都是同步呼叫。但是一般而言,我們在說同步、非同步的時候,特指那些需要其他部件協作或者需要一定時間完成的任務。
非同步:
一個程序在執行某個任務時,其他程序不必等待其執行完畢就能開始執行。當一個非同步功能呼叫發出後,呼叫者不能立刻得到結果。當該非同步功能完成後,通過狀態、通知或回撥來通知呼叫者。如果非同步功能用狀態來通知。那麼呼叫者就需要每隔一定時間檢查一次,效率就很低(有些初學多執行緒程式設計的人,總喜歡用一個迴圈去檢查某個變數的值,這其實是一 種很嚴重的錯誤)。如果是使用通知的方式,效率則很高,因為非同步功能幾乎不需要做額外的操作。至於回撥函式,其實和通知沒太多區別。
阻塞:
阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起(如遇到io操作)。函式只有在得到結果之後才會將阻塞的執行緒啟用。有人也許會把阻塞呼叫和同步呼叫等同起來,實際上他是不同的。對於同步呼叫來說,很多時候當前執行緒還是啟用的,只是從邏輯上當前函式沒有返回而已。
非阻塞:
不能立刻得到結果之前也會立刻返回,同時該函式不會阻塞當前執行緒。
總結:
- 同步與非同步針對的是函式/任務的呼叫方式:同步就是當一個程序發起一個函式(任務)呼叫的時候,一直等到函式(任務)完成,而程序繼續處於啟用狀態。而非同步情況下是當一個程序發起一個函式(任務)呼叫的時候,不會等函式返回,而是繼續往下執行當,函式返回的時候通過狀態、通知、事件等方式通知程序任務完成;
- 阻塞與非阻塞針對的是程序或執行緒:阻塞是當請求不能滿足的時候就將程序掛起,而非阻塞則不會阻塞當前程序。
二、阻塞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
未完待續……