IO在計算機中指Input/Output,也就是輸入和輸出。由於程序和運行時數據是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方,通常是磁盤、網絡等,就需要IO接口。
比如你打開瀏覽器,訪問新浪首頁,瀏覽器這個程序就需要通過網絡IO獲取新浪的網頁。瀏覽器首先會發送數據給新浪服務器,告訴它我想要首頁的HTML,這個動作是往外發數據,叫Output,隨后新浪服務器把網頁發過來,這個動作是從外面接收數據,叫Input。所以,通常,程序完成IO操作會有Input和Output兩個數據流。當然也有只用一個的情況,比如,從磁盤讀取文件到內存,就只有Input操作,反過來,把數據寫到磁盤文件里,就只是一個Output操作。
事件驅動模型
通常情況,有一下幾種情況模型:
- 每收到一個請求,創建一個新的進程,來處理該請求。
- 每收到一個請求,創建一個新的線程,來處理該請求。
- 每收到一個請求,放入一個時間列表,讓主進程通過非阻塞IO來處理請求。
綜上普遍認為第三種方式為大多數網絡服務器采用的方式。
例如在UI編程中,常常用到鼠標點擊進行操作,那么如何,何時去獲得鼠標的點擊進行處理呢?
在前面學到的線程中,我們可以創建一個線程對鼠標進行檢測。那么問題來了?
- CPU資源浪費,可能鼠標點擊的頻率非常小,但是掃描線程還是會一直循環檢測,這會造成很多的CPU資源浪費;如果掃描鼠標點擊的接口是阻塞的呢?
- 如果是堵塞的,又會出現下面這樣的問題,如果我們不但要掃描鼠標點擊,還要掃描鍵盤是否按下,由於掃描鼠標時被堵塞了,那么可能永遠不會去掃描鍵盤;
- 如果一個循環需要掃描的設備非常多,這又會引來響應時間的問題;
所以此方式是不可取的
第二種就是剛提到的事件驅動模型
目前大部分的UI編程都是事件驅動模型。如很多UI平台都會提供onClick()事件,這個事件就代表鼠標按下事件。事件驅動模型大體思路如下:
- 有一個消息隊列
- 鼠標點擊時,往這個隊列里增加一個點擊事件。
- 有個循環,不斷的從隊列中取出事件,根據不同事件調用不同的函數。
- 事件一般都各自保存各自的處理函數指針,這樣,每個消息都有獨立的處理函數。
簡單的代碼示例如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p onclick="func()">點我</p> <script> function func() { alert("你好") } </script> </body> </html>
圖形講解
事件驅動編程是一種編程范式,這里程序的執行流由外部事件來決定。它的特點是包含一個事件循環,當外部事件發生時使用回調機制來觸發相應的處理。另外兩種常見的編程范式是(單線程)同步以及多線程編程。
用個簡單的圖例來講解三者關系
圖中灰色部分表示I/O操作阻塞的時間
同步IO和異步IO
於CPU和內存的速度遠遠高於外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數據寫入磁盤,CPU輸出100M的數據只需要0.01秒,可是磁盤要接收這100M數據可能需要10秒,怎么辦呢?有兩種辦法:
- 第一種是CPU等着,也就是程序暫停執行后續代碼,等100M的數據在10秒后寫入磁盤,再接着往下執行,這種模式稱為同步IO;
- 另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不着急,我接着干別的事去了”,於是,后續代碼可以立刻接着執行,這種模式稱為異步IO。
同步和異步的區別就在於是否等待IO執行的結果。舉個例子來說。
好比你去麥當勞點餐,你說“來個漢堡”,服務員告訴你,對不起,漢堡要現做,需要等5分鍾,於是你站在收銀台前面等了5分鍾,拿到漢堡再去逛商場,這是同步IO。
你說“來個漢堡”,服務員告訴你,漢堡需要等5分鍾,你可以先去逛商場,等做好了,我們再通知你,這樣你可以立刻去干別的事情(逛商場),這是異步IO。
異步IO模型
異步IO全程過程沒有阻塞狀態。
注: 異步IO模塊3.0才有,叫asyncio
阻塞IO(blocking IO )
就上面的例子你去麥當勞點漢堡,但此時沒有了,要5分鍾后才出來,而你咬着等5分鍾,此刻的5分鍾就浪費了,這就是典型的阻塞IO。大概流程可以這樣表示
非阻塞IO(non-blocking IO)
假設你不想在那等,就出去辦其他的事了,但是你又擔心你點的漢堡被別人拿走了,你就來來回回好多趟,最后終於做好了。大概流程如下
需要注意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態
IO多路復用(IO multiplexing)
它的基本原理就是select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。就如我們點好餐后,不用每次都去問服務員了,看專門的顯示屏即可知道,那樣每個顧客都知道自己的等待時間了。它的流程如圖:
注意:當用戶進程調用了select時,那么整個進程就會阻塞住。看似和阻塞IO沒有太大的區別,但是這可以同時監聽處理多個connection。整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。
綜上所述,對幾種IO模型進行比較如下:
IO多路復用select,poll,epoll
select
select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用后select函數會阻塞,直到有描述符就緒(可讀、可寫、或except),或超時(timeout等待時間,如立即返回設為null即可),函數返回。當select函數返回后,可以通過遍歷fdset,來找到就緒的描述符。
select優勢在於幾乎支持所有平台。
缺點在於
- 單個進程能夠監視的文件描述符的數量存在最大限制,默認為1024.
- 每次select()都要輪詢遍歷FD_SETSIZE個Socket來完成調度,效率較低。
- 需要維護一個用來存放大量fd的數據結構,使得用戶空間和內核空間在傳遞時復制開銷大。
poll
相當於是select和epoll之間的過度,唯一的改動就是沒有了文件描述符的數量限制
epoll(linux特有的實現方法,目前windows不支持)
epoll有兩種模式,區別在當epoll_wait檢測到描述符事件發生並將此事件通知應用程序。
- LT模式:應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
- ET模式:應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。須使用非阻塞套接口
最后舉個並發聊天的例子如

import socket import select sk = socket.socket() sk.bind(('127.0.0.1', 8080)) sk.listen(3) sk_list = [sk, ] while True: r_list, w_list, x_list = select.select(sk_list, [], [],) for i in r_list: if i == sk: conn, addr = i.accept() #接收第一個傳進來的也就是sk,之后開始執行conn了 sk_list.append(conn) elif i == conn: data = conn.recv(1024) data = data.decode('utf8') try: # data is False if data is not False: print(data) inp = input("回復客戶端 %s >>>>" % sk_list.index(i)) conn.sendall(inp.encode('utf8')) except Exception: # print(e) sk_list.remove(i)

import socket sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.connect(('127.0.0.1', 8080)) while True: inp = input(">>>>") sk.sendall(inp.encode('utf8')) data = sk.recv(1024) print(data.decode('utf8'))