成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

從青銅到王者:帶你吃透IO模型

開發 前端
讓我們來看一個具體的案例,深入了解如何通過優化 IO 模型提升系統性能 。某在線教育平臺,其業務涵蓋了課程視頻播放、在線直播教學、用戶資料存儲等多個方面 。

在計算機編程的江湖中,IO 模型堪稱一位深藏不露卻又掌控全局的武林高手。如果把計算機程序比作一個龐大的江湖門派,那么 IO 操作就是門派中最基礎也最關鍵的 “內功心法”,而 IO 模型則是修煉這門 “內功” 的不同秘籍 。它看似低調,卻在幕后默默地決定著程序的性能、效率和穩定性,深刻影響著程序與外部世界(如文件系統、網絡等)交互的方式。

對于程序員來說,理解 IO 模型,就如同武林高手領悟上乘武功心法,是提升編程境界、突破技術瓶頸的必經之路。它不僅能幫助我們優化代碼,使其運行得更加高效流暢,還能讓我們在面對復雜的系統設計和性能調優問題時,有更清晰的思路和更強大的解決能力。今天,就讓我們一起深入探索 IO 模型的神秘世界,揭開它那層神秘的面紗。

一、IO模型簡介

1.1什么是 IO

在計算機世界里,IO(Input/Output)即輸入 / 輸出,堪稱計算機與外部世界溝通的橋梁,是數據在計算機內部與外部設備(如磁盤、網絡、鍵盤、顯示器等)之間的流動過程 ,就像人體的血脈,源源不斷地輸送著養分(數據)。從本質上講,IO 實現了數據在不同存儲介質或設備之間的遷移,讓計算機能夠獲取外部信息并輸出處理結果。

以磁盤 IO 為例,當我們運行一個程序時,如果程序所需的數據不在內存中,就會觸發磁盤 IO 操作,將數據從磁盤讀取到內存中。這一過程如同從倉庫(磁盤)中取出貨物(數據)并搬到工作區(內存)。而網絡 IO 則是在網絡通信中,數據在計算機與其他網絡節點之間的傳輸,比如我們瀏覽網頁時,計算機通過網絡 IO 從服務器獲取網頁數據,就像是從遠方的供應商那里接收貨物。

常見的IO模型包括阻塞IO、非阻塞IO、多路復用IO和異步IO:

  • 阻塞IO(Blocking IO):在執行一個IO操作時,如果數據沒有準備好,程序會一直等待,直到數據準備就緒才會返回結果。這種模型下,應用程序需要等待數據傳輸完成,期間無法進行其他任務。
  • 非阻塞IO(Non-blocking IO):在執行一個IO操作時,如果數據沒有準備好,程序不會等待而是立即返回。通過不斷輪詢來檢查是否有數據準備好,然后再進行讀寫操作。這樣可以避免長時間的阻塞等待,但仍然需要主動檢查狀態。
  • 多路復用IO(Multiplexing IO):使用select、poll或epoll等系統調用,在一個線程中同時監聽多個文件描述符(sockets),當任意一個文件描述符有就緒事件發生時,再去進行相應的讀寫操作。這樣可以同時處理多個連接,并且避免了頻繁地輪詢。
  • 異步IO(Asynchronous IO):在執行一個IO操作時,程序發起請求后就可以繼續做其他事情,并且在IO操作完成后,系統會通知應用程序進行數據處理。這種模型下,IO的讀寫操作和結果處理是分離的,應用程序無需等待IO操作完成。

1.2操作系統中的 IO “魔法”

在操作系統層面,IO 操作涉及用戶空間和內核空間的交互。用戶空間是應用程序運行的區域,而內核空間則負責管理硬件資源和提供系統服務。當應用程序發起 IO 請求時,比如調用read或write函數,這一請求會從用戶空間傳遞到內核空間。

具體來說,IO 調用首先由應用程序發起,發起后程序會等待系統內核完成實際的 IO 操作。以讀取網絡數據為例,內核先等待數據到達網卡,然后將數據拷貝至內核緩沖區,這是等待數據準備階段;接著,內核將數據從內核緩沖區拷貝到用戶空間,供應用程序處理,此為從內核向進程拷貝數據階段。這兩個階段構成了 IO 執行的全過程 ,而阻塞、非阻塞、異步、同步等概念,正是基于 IO 執行階段的不同狀態和處理機制而產生的。

二、五種IO模型詳解

2.1IO模型分析方法

分析IO模型需要了解2個問題:

問題1:發送IO請求,IO請求可以理解為用戶空間和內核空間數據同步,根據發起者不同分為以下兩種情況:

  • 由用戶程序發起(同步IO)。
  • 由內核發起(異步IO)。

問題2:等待數據到來,等待數據到來的方式有以下幾種:

  • 阻塞(阻塞IO)。
  • 輪詢(非阻塞IO)。
  • 信號通知(信號驅動IO)。

圖片圖片

(內核空間和用戶空間數據同步由誰發起是分析Linux IO模型最核心問題)

2.2 同步阻塞 IO:最基礎的 “老實人” 模型

同步阻塞 IO(Blocking IO)是最基礎、最容易理解的 IO 模型,堪稱 IO 世界里的 “老實人”。在這種模型下,當應用程序發起 IO 請求時,比如調用read函數讀取數據,程序會被阻塞,就像一個人在等待快遞送達,期間只能干巴巴地等著,什么也做不了 ,直到內核將數據準備好并拷貝到用戶空間,IO 操作完成,程序才會繼續執行后續代碼。

在 linux中,默認情況下所有的 socket都是blocking,一個典型的讀操作流程大概是這樣:

圖片圖片

當用戶進程調用了 recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據。

對于 network io來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候 kernel就要等待足夠的數據到來。

而在用戶進程這邊,整個進程會被阻塞。當 kernel一直等到數據準備好了,它就會將數據從 kernel中拷貝到用戶內存,然后 kernel返回結果,用戶進程才解除 block的狀態,重新運行起來;所以,blocking IO的特點就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被 block了。

幾乎所有的程序員第一次接觸到的網絡編程都是從 listen(),send(),recv(),等接口開始的,使用這些接口可以很方便的構建服務器/客戶機的模型。然而大部分的 socket接口都是阻塞型的,如下圖:

Tip:所謂阻塞型接口是指系統調用(一般是IO接口)不返回調用結果并讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。

圖片圖片

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

一個簡單地解決方案:

在服務器端使用多線程(或多進程)。
多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。

該方案的問題是:

開啟多進程或都線程的方式,在遇到要同時響應成百上千路的連接請求,則無論多線程還是多進程都會嚴重占據系統資源,
降低系統對外界響應效率,而且線程與進程本身也更容易進入假死狀態。

改進方案:

很多程序員可能會考慮使用“線程池”或“連接池”。
“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,并讓空閑的線程重新承擔新的執行任務。
“連接池”維持連接的緩存池,盡量重用已有的連接、減少創建和關閉連接的頻率。
這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種數據庫等。

改進后方案其實也存在著問題:

“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用IO接口帶來的資源占用。
而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應并不比沒有池的時候效果好多少。
所以使用“池”必須考慮其面臨的響應規模,并根據響應規模調整“池”的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,可以用非阻塞接口來嘗試解決這個問題。

2.3 同步非阻塞 IO:忙碌的 “輪詢者”

同步非阻塞 IO(Non-blocking IO)在同步阻塞 IO 的基礎上,將 socket 設置為非阻塞模式 ,就像一個忙碌的 “輪詢者”,不再傻傻地等待,而是不斷地詢問事情是否完成。當應用程序發起 IO 請求時,如果數據尚未準備好,函數會立即返回,而不會阻塞線程。但這并不意味著數據已經讀取成功,應用程序需要不斷地輪詢,再次發起 IO 請求,直到數據準備好并成功讀取 。

Linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

圖片圖片

從圖中可以看出,當用戶進程發出 read操作時,如果 kernel中的數據還沒有準備好,那么它并不會 block用戶進程,而是立刻返回一個 error。

從用戶進程角度講 ,它發起一個 read操作后,并不需要等待,而是馬上就得到了一個結果。

用戶進程判斷結果是一個 error時,它就知道數據還沒有準備好,于是用戶就可以在本次到下次再發起 read詢問的時間間隔內做其他事情,或者直接再次發送 read操作。

一旦 kernel中的數據準備好了,并且又再次收到了用戶進程的 system call,那么它馬上就將數據拷貝到了用戶內存(這一階段仍然是阻塞的),然后返回。

也就是說非阻塞的 recvform系統調用之后,進程并沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好,此時會返回一個 error。

進程在返回之后,可以干點別的事情,然后再發起 recvform系統調用。

重復上面的過程,循環往復的進行 recvform系統調用。這個過程通常被稱之為輪詢。

輪詢檢查內核數據,指導數據準備好,再拷貝數據到進程,進行數據處理。

需要注意,拷貝數據的整個過程,進程仍然是屬于阻塞的狀態。

所以,在非阻塞式 IO中,用戶進程其實是需要不斷的主動詢問 kernel數據準備好了沒有。

非阻塞 IO示例:

(1)服務端

# 實現自己監測IO,遇到IO,就切到我單個線程的其他用戶去運行了,實現單線程下的并發,并把單線程的效率提到了最高。
from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("192.168.2.209",8800))
server.listen(5)
server.setblocking(False)      # 默認是 True(阻塞),改成 False(非阻塞)
print("starting...")

rlist = []
wlist = []
while True:
    try:
        conn, addr = server.accept()
        rlist.append(conn)
        print(rlist)
    except BlockingIOError:
        # 收消息
        del_rlist = []  # 要刪除的鏈接
        for conn in rlist:
            try:
                data = conn.recv(1024)
                if not data:
                    del_rlist.append(conn)
                    continue
                # conn.send(data.upper())
                wlist.append((conn,data.upper()))
            except BlockingIOError:
                continue

            except Exception:
                conn.close()    # 服務端單方面斷開,這個鏈接就可以回收掉了
                del_rlist.append(conn)

        # 發消息
        del_wlist = []
        for item in wlist:
            try:
                conn = item[0]
                data = item[1]
                # 如果這里拋異常,那么下行代碼運行不了,如果沒拋異常,發成功了,就把鏈接加到刪除的列表
                conn.send(data)
                del_wlist.append(item)
            except BlockingIOError:
                pass

        for item in del_wlist:
            wlist.remove(item)

        for conn in del_rlist:
            rlist.remove(conn)

server.close()

(2)客戶端

from socket import *

client = socket(AF_INET,SOCK_STREAM)
client.connect(("192.168.2.209",8800))

while True:
    msg = input(">>:").strip()
    if not msg:continue
    client.send(msg.encode("utf-8"))
    data = client.recv(1024)
    print("收到的數據:%s" % data.decode("utf-8"))

client.close()

但是非阻塞 IO模型絕不被推薦。

我們不能否定其優點:能夠在等待任務完成的時間里干其他活了(包括提交其他任務,也就是 “后臺” 可以有多個任務在“”同時“”執行)。

但是也難掩其缺點:

循環調用 recv()將大幅度推高 CPU占用率;這也是我們在代碼中留一句time.sleep(2)的原因,
否則在低配主機下極容易出現卡機情況。

任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次 read操作,
而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數據吞吐量的降低。

此外,在這個方案中 recv()更多的是起到檢測“操作是否完成”的作用,實際操作系統提供了更為高效的檢測“操作是否完成“作用的接口,例如 select()多路復用模式,可以一次檢測多個連接是否活躍。

2.4IO 多路復用:高效的 “事件監聽者”

IO 多路復用(IO Multiplexing)模型是一種高效的 IO 模型,它就像是一個聰明的 “事件監聽者”,能夠同時監聽多個文件描述符(如 socket)的事件,當其中任何一個文件描述符就緒(有數據可讀、可寫或有異常)時,就會通知應用程序進行相應的處理 。這使得一個線程可以處理多個 IO 請求,大大提高了程序的并發處理能力。

IO 多路復用的實現方式主要有select、poll和epoll。select函數通過監聽文件描述符集合,當有描述符就緒時返回,通知應用程序進行處理 。但它有一些局限性,比如能監聽的文件描述符數量有限(通常為 1024 個),每次調用select都需要將文件描述符集合從用戶空間拷貝到內核空間,并且返回時需要遍歷整個集合來判斷哪些描述符就緒,效率較低 。poll函數與select類似,但它使用鏈表來存儲文件描述符,突破了文件描述符數量的限制,但在處理大量文件描述符時,性能仍然會隨著描述符數量的增加而下降 。

epoll是select和poll的增強版,它采用事件驅動的方式,在內核中維護一個事件表,當有事件發生時,直接將事件通知給應用程序,而不需要遍歷整個文件描述符集合 。epoll還支持水平觸發(Level Triggered,LT)和邊緣觸發(Edge Triggered,ET)兩種模式,其中邊緣觸發模式的效率更高,適用于高并發場景 。

它的基本原理就是 select/epoll這個 function會不斷的輪詢所負責的所有 socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

圖片圖片

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

強調:

  • 如果處理的連接數不是很高的話,使用 select/epoll的 web server不一定比使用 multi-threading + blocking IO的 web server性能更好,可能延遲還更大。select/epoll的優勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。
  • 在多路復用模型中,對于每一個 socket,一般都設置成為 non-blocking,但是,如上圖所示,整個用戶的 process其實是一直被 block的。只不過 process是被 select這個函數 block,而不是被 socket IO給 block。

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

select網絡IO模型示例:

from socket import *
import select

server = socket(AF_INET, SOCK_STREAM)
server.bind(("192.168.2.209",9900))
server.listen(5)
server.setblocking(False)

rlist = [server,]   # 收,存一堆conn,和server
wlist = []          # 發,存一堆conn,一旦緩存區沒滿,證明可以發了
wdata = {}
while True:
    rl,wl,xl = select.select(rlist,wlist,[],1)   # 后面的列表是出異常的列表,1是超時時間,每隔1s問一遍操作系統
    print("rl",rl)      # 收的套接字
    print("wl",wl)      # 發的套接字

    for sock in rl:
        if sock == server:
            conn,addr = sock.accept()
            rlist.append(conn)
        else:
            try:    # recv的時候,客戶端單方面的把鏈接斷開,會拋異常,在這里捕獲下
                data = sock.recv(1024)
                if not data:    # 客戶端沒發數據,跳過
                    sock.close()
                    rlist.remove(sock)
                    continue
                wlist.append(sock)
                wdata[sock] = data.upper()
            except Exception:
                sock.close()
                rlist.remove(sock)

    for sock in wl:
        data = wdata[sock]
        sock.send(data)
        wlist.remove(sock)
        wdata.pop(sock)

server.close()

select監聽fd變化的過程分析:

用戶進程創建socket對象,拷貝監聽的fd到內核空間,每一個fd會對應一張系統文件表,內核空間的fd響應到數據后,
就會發送信號給用戶進程數據已到;
用戶進程再發送系統調用,比如(accept)將內核空間的數據copy到用戶空間,同時作為接受數據端內核空間的數據清除, 
這樣重新監聽時fd再有新的數據又可以響應到了(發送端因為基于TCP協議所以需要收到應答后才會清除)。

該模型的優點:

相比其他模型,使用select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,
同時能夠為多客戶端提供服務。如果試圖建立一個簡單的事件驅動的服務器程序,這個模型有一定的參考價值。

該模型的缺點:

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

2.5異步IO

Linux下的 asynchronous IO其實用得不多,從內核2.6版本才開始引入。先看一下它的流程:

圖片圖片

用戶進程發起 read操作之后,立刻就可以開始去做其它的事,而另一方面,從 kernel的角度,當它受到一個 asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產生任何 block。

然后,kernel會等待數據準備完成,然后將數據拷貝到用戶內存,當這一切都完成之后,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

2.6IO模型比較分析

到目前為止,已經將四個 IO Model都介紹完了。現在回過頭來回答最初的那幾個問題:blocking和non-blocking的區別在哪,synchronous IO和asynchronous IO的區別在哪。

先回答最簡單的這個:blocking vs non-blocking。前面的介紹中其實已經很明確的說明了這兩者的區別。

調用 blocking IO會一直block住對應的進程直到操作完成,而non-blocking IO在kernel還準備數據的情況下會立刻返回。

再說明 synchronous IO和asynchronous IO的區別之前,需要先給出兩者的定義。Stevens給出的定義(其實是POSIX的定義)是這樣子的:A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;An asynchronous I/O operation does not cause the requesting process to be blocked;兩者的區別就在于 synchronous IO做”IO operation”的時候會將 process阻塞。

按照這個定義,四個IO模型可以分為兩大類,之前所述的 blocking IO,non-blocking IO,IO multiplexing都屬于synchronous IO這一類,而 asynchronous I/O后一類 。

有人可能會說,non-blocking IO并沒有被 block啊。這里有個非常“狡猾”的地方,定義中所指的”IO operation”是指真實的IO操作,就是例子中的 recvfrom這個 system call。

non-blocking IO在執行recvfrom這個system call的時候,如果kernel的數據沒有準備好,這時候不會block進程。

但是,當 kernel中數據準備好的時候,recvfrom會將數據從 kernel拷貝到用戶內存中,這個時候進程是被 block了,在這段時間內,進程是被 block的。

而 asynchronous IO則不一樣,當進程發起 IO操作之后,就直接返回再也不理睬了,直到 kernel發送一個信號,告訴進程說 IO完成。在這整個過程中,進程完全沒有被 block。

各個 IO Model的比較如圖所示:

圖片圖片

經過上面的介紹,會發現 non-blocking IO和 asynchronous IO的區別還是很明顯的。

在 non-blocking IO中,雖然進程大部分時間都不會被 block,但是它仍然要求進程去主動的 check,并且當數據準備完成以后,也需要進程主動的再次調用 recvfrom來將數據拷貝到用戶內存。

而 asynchronous IO則完全不同。它就像是用戶進程將整個 IO操作交給了他人(kernel)完成,然后他人做完后發信號通知。在此期間,用戶進程不需要去檢查 IO操作的狀態,也不需要主動的去拷貝數據。

三、走進IO模型的世界

3.1同步與異步:任務協作的不同策略

同步和異步,是 IO 模型中的兩種基本協作模式,它們決定了程序在處理任務時的執行順序和響應方式。簡單來說,同步就像是一場按部就班的接力賽,每個選手必須等前一個選手完成交接后才能出發;而異步則更像一場并行的馬拉松,選手們可以各自按照自己的節奏奔跑,不需要相互等待。

在日常生活中,同步和異步的例子隨處可見。比如你去餐廳點餐,如果你選擇坐在餐桌前等待服務員做好餐食并端上桌,這就是同步的方式 —— 你必須等待當前的點餐任務完成(餐食做好并送達),才能進行下一步行動,比如開始用餐。而如果你選擇點外賣,下單后你可以繼續做其他事情,等外賣送到時再去取餐,這便是異步的過程 —— 你不需要一直等待外賣送達,在等待過程中可以并行處理其他事務 。

在代碼世界里,同步和異步的區別也十分明顯。以 Python 代碼為例:

import time


# 同步函數
def synchronous_task():
    print("同步任務開始")
    time.sleep(2)  # 模擬耗時操作
    print("同步任務結束")


# 異步函數
async def asynchronous_task():
    print("異步任務開始")
    await asyncio.sleep(2)  # 模擬耗時操作
    print("異步任務結束")


# 同步調用
print("開始同步調用")
synchronous_task()
print("同步調用結束")

# 異步調用(使用asyncio庫)
import asyncio

print("開始異步調用")
asyncio.run(asynchronous_task())
print("異步調用結束")

在上述代碼中,synchronous_task是一個同步函數,當調用它時,程序會阻塞在time.sleep(2)這一行,直到 2 秒后函數執行完畢,才會繼續執行后續代碼。而asynchronous_task是一個異步函數,await asyncio.sleep(2)雖然也模擬了 2 秒的耗時操作,但在這 2 秒內,程序并不會阻塞,而是可以去執行其他異步任務,當asyncio.sleep(2)完成后,才會繼續執行asynchronous_task函數后續的代碼。

在 IO 操作中,同步和異步的表現也截然不同。同步 IO 操作會導致程序阻塞,直到 IO 操作完成。例如,當我們使用requests庫發送 HTTP 請求獲取網頁內容時:

import requests

response = requests.get('https://www.example.com')  # 同步請求,程序會阻塞在這里
print(response.text)

在這段代碼中,程序會在requests.get這一行阻塞,直到服務器響應并返回網頁內容,期間程序無法執行其他任務。

而異步 IO 操作則不會阻塞程序。在 Python 的aiohttp庫中,我們可以實現異步的 HTTP 請求:

import aiohttp
import asyncio


async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()


async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://www.example.com')
        print(html)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在這個例子中,fetch函數使用aiohttp庫進行異步的 HTTP 請求。在請求過程中,程序不會阻塞,可以繼續執行其他異步任務,當請求完成后,才會處理返回的結果。這種方式大大提高了程序的執行效率,尤其是在處理大量 IO 操作時,能夠充分利用系統資源,減少等待時間。

3.2阻塞與非阻塞:等待數據的不同姿態

阻塞與非阻塞,描述的是程序在等待 IO 操作完成時的狀態。阻塞就像是在交通擁堵的道路上,車輛必須停下來等待前方道路暢通;非阻塞則如同在暢通無阻的高速公路上,車輛可以自由行駛,無需等待。

在 IO 操作中,阻塞 IO 是最常見的方式。當一個進程發起阻塞 IO 操作時,如果數據尚未準備好,該進程會被掛起,進入等待狀態,直到數據準備就緒并完成 IO 操作,進程才會被喚醒繼續執行 。例如,使用read函數從文件中讀取數據:

try:
    with open('example.txt', 'r') as file:
        data = file.read()  # 阻塞IO操作,如果文件讀取緩慢,程序會在此阻塞
        print(data)
except FileNotFoundError:
    print("文件未找到")

在這段代碼中,如果example.txt文件較大或者磁盤讀取速度較慢,file.read()操作會阻塞程序,直到數據讀取完成。在阻塞期間,程序無法執行其他任務,CPU 資源被浪費。

非阻塞 IO 則不同,當一個進程發起非阻塞 IO 操作時,如果數據尚未準備好,函數會立即返回一個錯誤碼或狀態標識,而不會阻塞進程。進程可以繼續執行其他任務,然后通過輪詢等方式再次檢查數據是否準備好 。在 Python 中,可以使用socket模塊實現非阻塞 IO:

import socket


# 創建一個socket對象
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)  # 設置為非阻塞模式

try:
    # 嘗試連接服務器
    sock.connect(('www.example.com', 80))
except BlockingIOError:
    pass  # 連接未完成,繼續執行其他任務

# 可以在此處執行其他任務

# 輪詢檢查連接是否完成
while True:
    try:
        sock.send(b'GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n')
        break
    except BlockingIOError:
        continue

# 接收數據
data = sock.recv(1024)
print(data.decode('utf-8'))
sock.close()

在上述代碼中,sock.setblocking(False)將 socket 設置為非阻塞模式。在調用connect方法時,如果連接不能立即建立,程序不會阻塞,而是繼續執行后續代碼。然后通過輪詢的方式,不斷嘗試發送數據,直到連接成功。這種方式提高了程序的并發性,使得程序在等待 IO 操作的過程中可以執行其他任務,充分利用了 CPU 資源 。

實際應用中,阻塞 IO 適用于對響應時間要求不高、IO 操作較少且數據量小的場景,例如簡單的文件讀取、本地數據庫查詢等。而非阻塞 IO 則更適合高并發、對響應時間要求較高的場景,如網絡服務器的并發處理、實時數據采集等。在這些場景中,非阻塞 IO 能夠避免線程被大量阻塞,提高系統的吞吐量和響應速度。

四、IO模型常見問題解析

4.1阻塞IO和非阻塞IO區別?

a.阻塞IO:在阻塞IO模型中,當應用程序發起一個IO操作(例如讀取文件或網絡數據),如果數據沒有準備好,該操作會一直阻塞線程的執行。線程會等待,直到數據準備好后才能繼續執行后續的代碼。這意味著在進行阻塞IO操作期間,線程無法執行其他任務。

b.非阻塞IO:相比之下,非阻塞IO模型允許應用程序在發起一個IO操作后立即返回,并且無論數據是否準備好都可以繼續執行后續的代碼。如果數據還沒有準備好,非阻塞IO模型會立即返回一個錯誤碼或者空數據。應用程序需要通過輪詢或者重復嘗試來檢查數據是否已經準備好。

非阻塞IO與阻塞IO相比具有以下特點:

  1. 異步通知:在非阻塞IO模型中,應用程序可以使用回調函數或者事件驅動機制來處理IO操作完成的通知。當數據準備就緒時,系統會通知應用程序進行讀取或寫入操作。
  2. 非阻塞輪詢:為了檢查是否有數據可用,應用程序需要通過輪詢的方式重復檢查IO狀態。這意味著在沒有數據可讀取時,應用程序需要不斷地輪詢以避免線程被阻塞。
  3. 處理更多任務:由于非阻塞IO模型允許應用程序同時處理多個任務,在等待一個任務的結果時可以繼續執行其他任務。這可以提高并發性和吞吐量。

4.2同步IO和異步IO區別?

同步IO(Synchronous IO):在同步IO模型中,應用程序發起一個IO操作后會被阻塞等待操作完成。這意味著應用程序必須等待IO操作完成后才能繼續執行后續代碼。當數據就緒時,系統將數據傳輸給應用程序并解除阻塞狀態,應用程序可以讀取或寫入數據。同步IO通常以函數調用的形式進行,例如讀取文件、網絡請求等。

異步IO(Asynchronous IO):在異步IO模型中,應用程序發起一個IO操作后立即返回,并不會阻塞等待結果返回。應用程序可以繼續執行后續代碼而無需等待。當數據就緒時,系統會通知應用程序進行讀取或寫入操作,并提供相應的回調函數或事件處理機制來處理完成通知。異步IO常見的實現方式包括回調函數、Promise/Future對象、協程/生成器等。

關鍵區別:

  • 同步IO需要阻塞等待結果返回,而異步IO則不需要。
  • 同步IO只能進行一次性的單個操作,而異步IO可以同時處理多個任務。
  • 同步IO適合于簡單和可預測的IO操作,而異步IO適合于需要同時處理大量任務和高并發的場景。

4.3信號驅動IO和異步IO區別?

信號驅動IO:信號驅動IO使用操作系統提供的信號機制來通知應用程序IO事件的發生。應用程序首先通過調用sigaction()函數注冊一個信號處理函數,并指定需要監聽的IO事件,如可讀、可寫等。當指定的事件發生時,操作系統會發送相應的信號給應用程序,并執行注冊的信號處理函數進行后續操作。這種模型下,應用程序可以繼續執行其他任務而無需等待結果返回。

異步IO:異步IO則是通過操作系統提供的異步IO接口進行實現。應用程序通過向內核發起異步IO請求,并傳入回調函數或其他形式的完成通知方式。當數據就緒時,內核會主動通知應用程序并觸發回調函數或完成事件來進行后續操作。在異步IO模型中,應用程序可以并行地執行其他任務,而不需要一直等待結果返回。

關鍵區別:

  • 信號驅動IO使用了操作系統提供的信號機制,而異步IO則使用了專門設計的異步接口。
  • 信號驅動IO以信號為觸發點進行通知,而異步IO則以回調函數或事件為觸發點進行通知。
  • 信號驅動IO的實現相對較為底層,需要應用程序自行處理信號和回調函數,而異步IO則提供了更高級的接口封裝。

選擇使用信號驅動IO還是異步IO取決于具體應用的需求和平臺支持情況。在某些平臺上,可能更適合使用信號驅動IO來處理特定類型的事件。而在其他情況下,使用操作系統提供的異步IO接口能夠更方便地實現非阻塞IO操作。

4.4非阻塞IO是不是異步IO?

非阻塞IO(Non-blocking IO)和異步IO(Asynchronous IO)是兩種不同的IO操作模型,盡管它們都用于實現非阻塞式的IO處理,但在實現機制上有所不同。

非阻塞IO指的是應用程序在進行IO操作時,如果沒有立即得到所需的數據或結果,不會一直等待而是立即返回一個錯誤碼。應用程序可以通過輪詢或選擇性地調用其他任務來繼續執行,然后再次檢查IO是否就緒。這種模型下,應用程序需要主動查詢和處理IO狀態。

異步IO則是一種更高級的模型,在發起一個IO請求后,應用程序無需一直等待結果返回,而是可以繼續執行其他任務。當數據就緒或操作完成時,操作系統會通知應用程序,并觸發事先注冊好的回調函數或者發送事件通知。這種模型下,應用程序可以并行執行多個任務,并且在適當時候接收通知。

盡管非阻塞IO和異步IO都能夠實現非阻塞式的IO處理,但其實現方式和編程模型上有所差異。非阻塞IO需要應用程序自行查詢和處理狀態變化,而異步IO則由操作系統負責監測和通知狀態變化。因此,雖然非阻塞IO可以視為一種基本形式的異步IO,但兩者在編程模型和使用方式上還是有區別的。

五、優化IO性能的實戰經驗

讓我們來看一個具體的案例,深入了解如何通過優化 IO 模型提升系統性能 。某在線教育平臺,其業務涵蓋了課程視頻播放、在線直播教學、用戶資料存儲等多個方面 。在平臺發展初期,用戶量較少,系統采用了同步阻塞 IO 模型來處理網絡請求和文件讀寫操作 ,這種簡單直接的方式在當時能夠滿足業務需求 。

然而,隨著平臺知名度的提升,用戶數量急劇增加,特別是在黃金時間段,大量用戶同時在線觀看課程視頻和參與直播,系統開始出現嚴重的性能問題 。視頻播放卡頓、直播延遲明顯,用戶投訴不斷 。經過深入分析,發現同步阻塞 IO 模型在高并發情況下,線程頻繁阻塞,導致系統資源利用率低下,無法及時處理大量的請求 。

為了解決這些問題,開發團隊決定對系統進行 IO 模型的優化 。他們將核心的網絡請求處理部分從同步阻塞 IO 模型切換為 IO 多路復用模型(采用 epoll 實現) ,并結合異步 IO 來處理文件讀寫操作,尤其是在視頻文件的讀取和緩存方面 。具體來說,在網絡請求處理上,通過 epoll 監聽多個客戶端連接,當有請求到達時,迅速將其分配到線程池中進行處理,大大提高了并發處理能力 。在視頻文件讀取時,采用異步 IO,提前將熱門視頻文件異步加載到內存緩存中,當用戶請求播放視頻時,可以直接從緩存中讀取數據,減少了磁盤 IO 的等待時間 。

優化后,系統性能得到了顯著提升 。視頻播放卡頓現象幾乎消失,直播延遲降低到了可接受的范圍內,用戶滿意度大幅提高 。同時,服務器的資源利用率也得到了優化,能夠以更少的硬件資源支持更多的用戶并發訪問 。從這個案例中,我們可以總結出以下經驗教訓:在系統設計初期,就應該充分考慮業務的發展趨勢和可能面臨的并發場景,選擇合適的 IO 模型,避免后期因性能問題進行大規模的架構調整 。在優化 IO 模型時,要綜合考慮系統的各個環節,不僅僅是網絡請求處理,還包括文件讀寫、數據緩存等,進行全面的優化才能取得最佳效果 。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-04-27 02:33:00

epoll核心機制服務器

2020-05-10 18:02:42

機器學習神經網絡深度學習

2018-03-22 04:48:06

2019-12-10 14:50:54

創業技術CTO

2017-07-27 09:54:06

MySQL數據庫

2017-08-31 16:26:06

數據庫MySQL命令

2022-12-23 14:29:18

團隊Leader

2022-11-25 10:01:02

團隊敏捷團隊

2020-02-07 11:07:53

數組鏈表單鏈表

2022-10-27 12:15:20

DLP技術數據自主保護

2025-05-26 03:20:00

2019-05-07 17:31:57

華為

2025-05-06 08:20:00

互斥鎖C++編程

2020-12-07 14:48:15

Python開發工具

2022-10-13 17:43:10

MySQL存放數據

2021-01-04 05:53:35

MyBatis底層Java

2025-06-05 03:11:00

2021-04-29 19:07:33

Redis演進微服務

2023-11-17 09:00:00

Kafka開發

2020-09-23 12:32:18

網絡IOMySQL
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美精品在线免费观看 | 青青草在线视频免费观看 | 日本黄色大片免费看 | 国产成人99久久亚洲综合精品 | 一区二区三区免费在线观看 | 免费小视频在线观看 | 91动漫在线观看 | 欧美自拍日韩 | 日本高清视频网站 | 欧美高清一区 | 日韩中文字幕在线视频 | 国产真实精品久久二三区 | gav成人免费播放视频 | 亚洲福利视频网 | 亚洲精品乱码久久久久久蜜桃91 | 日韩精品久久久久久 | 精品日韩一区二区 | 国产精品大全 | 老司机午夜性大片 | 成人日韩精品 | 日日碰狠狠躁久久躁婷婷 | 欧美一区二区三区大片 | 久久成人精品视频 | 国产福利视频 | 国产精品久久久久久 | 91电影在线播放 | 欧美一区二区三区在线视频 | 欧美日韩大陆 | 免费观看黄色片视频 | 逼逼网 | 91av免费版 | 久久久久成人精品 | 天天想天天干 | 久草新在线 | 欧美一区二区三区在线播放 | 欧美日本韩国一区二区 | 日韩电影免费观看中文字幕 | 精品国产乱码久久久久久牛牛 | 中文字幕在线观看一区 | 国产黄色网 | 美国十次成人欧美色导视频 |