IO模型介紹:html
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路複用
* signal driven IO 信號驅動IO ()
* asynchronous IO 異步IOpython
爲了更好地瞭解IO模型,咱們須要事先回顧下:同步、異步、阻塞、非阻塞linux
同步(synchronous) IO和異步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分別是什麼,到底有什麼區別?這個問題其實不一樣的人給出的答案均可能不一樣,好比wiki,就認爲asynchronous IO和non-blocking IO是一個東西。這實際上是由於不一樣的人的知識背景不一樣,而且在討論這個問題的時候上下文(context)也不相同。因此,爲了更好的回答這個問題,我先限定一下本文的上下文。django
本文討論的背景是Linux環境下的network IO。本文最重要的參考文獻是Richard Stevens的「UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking 」,6.2節「I/O Models 」,Stevens在這節中詳細說明了各類IO的特色和區別,若是英文夠好的話,推薦直接閱讀。Stevens的文風是有名的深刻淺出,因此不用擔憂看不懂。本文中的流程圖也是截取自參考文獻。windows
Stevens在文章中一共比較了五種IO Model:
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路複用
* signal driven IO 信號驅動IO
* asynchronous IO 異步IO
由signal driven IO(信號驅動IO)在實際中並不經常使用,因此主要介紹其他四種IO Model。網絡
再說一下IO發生時涉及的對象和步驟。對於一個network IO (這裏咱們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另外一個就是系統內核(kernel)。當一個read操做發生時,該操做會經歷兩個階段:app
#1)等待數據準備 (Waiting for the data to be ready) #2)將數據從內核拷貝到進程中(Copying the data from the kernel to the process)
記住這兩點很重要,由於這些IO模型的區別就是在兩個階段上各有不一樣的狀況。框架
#同步:提交一個任務以後,要等待這個任務執行完畢 #好比去銀行存錢,要取完號才能存錢 #異步:只管提交任務,不等待這個任務執行完畢,就能夠作其餘事情 #能夠邊存錢,邊玩手機 #阻塞:recv recvfrom accpt 取號的時候,等待人不少 #非阻塞: #不少種狀況了 #recv 等待數據準備,等待數據從內核拷貝到進程 #send 講用戶要發送的信息copy到操做系統 發送的時候的網絡延遲 #主動方因此阻塞很小
#圖在 有道詞典 20181001 IO模型 #1.tcp/udp recv的時候—系統調用—內核操做系統數據沒有準備好—等待數據------->數據準備好了,copy數據,將操做系統中的數據,複製,而且返回給進程 #兩個重要節點:1.數據準備,2.將信息從內核拷貝到進程 #阻塞IO特色:程序被阻塞着,接收不了其餘任務 #進程和線程,沒有解決阻塞的這段時間,只是在開啓多個進程/線程,該等仍是在等,每一個線程都受到了阻塞的影響 #協程只是必定程度上的解決這個問題,可是仍是在等待
#圖在有道詞典20181001 IO模型 #非阻塞IO #用戶端: #recv ----> 告訴系統我要收數據了 ----> 系統告訴你沒數據---->而後程序沒有阻塞住,能夠繼續日後執行,可是也沒收到數據 #過段時間,繼續追問 #recv----> 而後系統繼續告訴你沒有數據,重複上面過程 #假設,一直在請求,請求了不少次,直到 #recv----> 有數據了,----> 從操做系統copy到進程中,而後告訴用戶,有數據了 # 可是非阻塞IO模型毫不被推薦。 # 咱們不可否則其優勢:可以在等待任務完成的時間裏幹其餘活了(包括提交其餘任務,也就是 「後臺」 能夠有多個任務在「」同時「」執行)。 # 可是也難掩其缺點: #1. 循環調用recv()將大幅度推高CPU佔用率;這也是咱們在代碼中留一句time.sleep(2)的緣由,不然在低配主機下極容易出現卡機狀況 #2. 任務完成的響應延遲增大了,由於每過一段時間纔去輪詢一次read操做,而任務可能在兩次輪詢之間的任意時間完成。這會致使總體數據吞吐量的下降。 # 此外,在這個方案中recv()更多的是起到檢測「操做是否完成」的做用, # 實際操做系統提供了更爲高效的檢測「操做是否完成「做用的接口,例如select()多路複用模式,能夠一次檢測多個鏈接是否活躍。
非阻塞IO框架,例子及解釋:
import socket sk = socket.socket() sk.bind(('127.0.0.1',8088)) sk.setblocking(False) #~~~~~~~~~~~~~~將阻塞設置爲不阻塞 sk.listen() conn_lst = [] del_lst = [] while True: try: conn,addr = sk.accept() print('創建了鏈接',addr) conn_lst.append(conn) # msg = conn.recv(1024).decode('utf8') #註釋1 # print(msg) except BlockingIOError: for con in conn_lst: #循環反覆的獲取con try: msg = con.recv(1024) # 這裏仍是非阻塞,仍是會報錯 if msg == b'': # 可是若是一直從一個關閉的client獲取數據,會一直打印空 del_lst.append(con) #不能 conn_lst.remove(con) 不能再for循環中列表裏的元素,index會錯誤 continue #當con傳過來爲空,就不執行後面的語句了,要進行下一個循環 print(msg) con.send(b'bye') except BlockingIOError:pass for con in del_lst: con.close() conn_lst.remove(con) del_lst.clear() #註釋1: #此處有兩種狀況,第一種因爲我此時是不阻塞的(setblocking)client端以很是快速的和我聊天,致使while循環就沒效果了,別人接收不了請求了 #可是若是隻快速的發了一句,而後server就只能接收這一句,接下來就會由於非阻塞,而後這個conn就被內存給沖掉了 #第二種因爲此時是阻塞的,client若是慢了一點給我傳消息,我立刻會跳過,而且報錯BlockingIOError #解決方法: #創建一個conn列表,把每次鏈接成功的conn,放到列表裏。而且把recv放到 except語句後,由於沒信息就會報錯。 #而後for循環這個conn列表,一直去嘗試獲取,conn是否發了消息過來 #從而讓原本本內存刷掉的conn,能夠一直被嘗試獲取 #可是此是for循環的列表裏,recv仍是非阻塞的,因此要繼續異常處理
import time import socket import threading def func(): sk = socket.socket() sk.connect(('127.0.0.1', 8088)) sk.send(b'hello') time.sleep(0.1) print(sk.recv(1024)) sk.close() for i in range(20): threading.Thread(target=func).start()
#select 模塊 #圖在有道 # IO多路複用:操做系統級別的,windows的select機制,不是咱們代碼提供的 # 流程: #1.有個代理(select模塊),能夠幫助你監聽一個對象。並且這個代理能夠監聽多個對象 conn1,2,3,4... #能夠接受數據的對象,好比 socket.accept conn.recv #發生一次系統調用, #2.而後這個代理,會替你等待 socket對象 被鏈接,若是沒有人來連,會一直阻塞 (從而監聽對象就能夠不阻塞了) #3.阻塞到有client來鏈接,而後把 反饋信息 給幫忙監聽的對象,好比accept #2-3步驟,是操做系統幫你循環監聽列表,查看每一項是否有可讀事件(但不是很高效) #4.而後accept知道了有數據來了,再次產生一次系統調用,找操做系統要數據 #5.系統就複製數據,傳給了一開始索要數據的對象 #注意: 1~3步驟纔是IO多路複用,4~5步就是 對象正常獲取數據的步驟了 #監聽方式: #windows 只有 select機制 #linux : # select機制: 都是操做系統輪詢每個被監聽的對象,看是否有讀操做 #poll機制: 和select機制同樣,可是poll好在能夠監聽的對象,比select機制多 #好比select能夠監聽500個,poll能夠監聽1000個 #隨着監聽項的增多,會致使監聽效率下降。(好比監聽列表有1000個,當我監聽完第3個了,第2個來消息了,) #epoll機制: #給監聽的每個對象,都綁定了一個回調函數 #每當有可讀事件,回調函數就立刻進行信息反饋,而不用等待輪詢 #很高效,能夠在Linux上用 # selectors模塊,自動幫你選擇,你最適用的監聽機制,根本不須要關心
IO多路複用框架,例子及解釋:
import socket import select sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.setblocking(False) sk.listen() read_lst = [sk] while True: r_lst,w_lst,x_lst = select.select(read_lst,[],[]) #read_lst中的監聽對象,只有有迴應的,纔會出如今 r_lst print('***',r_lst) # 註釋1 for i in r_lst: if i is sk: conn,addr = sk.accept() read_lst.append(conn) else: ret = i.recv(1024) if ret == b'': i.close() read_lst.remove(i) # 此時 r_lst和read_lst是兩個不一樣內存空間,能夠remove continue print(ret) i.send(b'goodbye') #註釋1 #此時經歷過一次鏈接後,read_lst原本是有着 [conn,sk], 而後此時若是有人給我發消息,也就是我此時 conn.recv了 #因爲 select模塊, 此時的r_lst只有 conn了,由於沒有人鏈接我,sk也就不會出如今 r_lst裏
import time import socket import threading def func(): sk = socket.socket() sk.connect(('127.0.0.1',8080)) sk.send(b'hello') time.sleep(3) print(sk.recv(1024)) sk.close() for i in range(20): threading.Thread(target=func).start()
#異步IO #步驟 #1. 阻塞對象,recv,accept,告訴系統,我要數據,而後接着就去幹別的事了,不阻塞 #2.操做系統就在那邊阻塞等待着數據,等到直到有數據傳來了 #3.而後操做系統,直接將數據傳給用戶。 # (!!)可是python在這一步,沒有提供這個copy data沒有提供python對操做系統的接口 #因此不能用python代碼實現,真正的異步IO模型 #可是c語言能夠,咱們可使用不少異步框架來實現 #4.用戶收到數據了。 #django就不是異步框架 #異步框架:沒有waitdata和copydata的阻塞階段,能夠響應更多請求 # twisted框架 #tornado框架 # tornado 和 twisted,做爲異步框架,是大同小異的。 # 只不過tornado 輕量級一些,twisted 重量級一些。在其餘方面,也是互有長短。 # 通過實測,發現這兩個框架,I/O性能差很少,對計算資源的佔用相差較多! # 若是追求總體性能的話,推薦使用twisted。