I/O完成端口是個什麼鬼

I/O完成端口解決什麼問題?

大的應用背景是異步I/O。什麼是異步I/O呢?先說說同步I/O吧。一個線程在讀取或者寫入文件的時候,如果I/O沒有完成,線程就要一直等待,幹不了其他事情,這就是同步I/O。對於聰明而急性子的計算機科學家來說,這種讀寫方式也太low了,根本不能帶來碾壓衆人智商的快感。於是異步I/O誕生了。它允許線程發出I/O請求後還能做其他事情。但是又帶來了新的問題,I/O請求完成後如何通知線程處理。

 

線程有四種方式接收I/O請求完成通知 

一、觸發設備內核對象方式

線程向I/O設備發出I/O請求的同時,將設備置爲未觸發狀態,I/O完成後將自己設置爲觸發狀態。線程將設備置爲未觸發狀態後就一直盯着設備狀態,發現是觸發後知道I/O完成了。

這個方式還是很low,相當於線程將設備的一個小旗子降下來,然後盯着設備再升起小旗子。線程盯着設備小旗子的時候實際也是啥都沒做,所以這種方式基本沒用。

 

二、觸發事件內核對象

原理和第一個差不多,只不過不再盯着I/O設備的小旗子,而是爲每個I/O請求專門創建一個事件內核對象,靠着擺弄它的小旗子協調異步I/O。

它的缺點在於每次I/O都要創建一個事件內核對象,你說囉嗦不羅嗦。

 

三、可提醒I/O

終於來了個有點技術含量的。

首先你要記住,每個線程天生自帶一個異步過程調用(APC)隊列,本質上是一個回調函數隊列。

可提醒I/O的工作過程是這樣的。發出I/O請求的線程需要自定義一個回調函數,然後發出I/O請求以後它可以先去做別的事情。在設備處理完I/O請求以後,會在發出I/O請求的線程的APC隊列中添加一項,包含了回調函數和一些相關參數。再說剛纔的線程,它忙別的事情總要達到一個點,這時它必須對I/O結果進行處理。

這裏就有幾個問題需要討論:第一,線程如何知道I/O完成了;第二,線程去哪裏處理I/O完成項。線程可以調用6個可以將線程置爲可提醒狀態的函數。調用完以後,操作系統檢查發現它的APC隊列有未處理項,這種情況下線程就處於可提醒狀態(而如果調用完那6個函數後,線程的APC隊列爲空,線程將進入睡眠狀態),意思是操作系統會提醒線程去處理它的APC隊列,這就實現了第一點。線程執行那個回調函數,處理I/O完成項,就實現了第二點。

簡單的說,線程發出I/O請求後,還要自定義一個回調函數,這個回調函數在I/O設備完成I/O請求後被I/O設備添加到線程的APC隊列,等待最後由線程自己完成處理。

可提醒I/O的缺點有兩個:

1.線程必須創建一個回調函數處理I/O完成項,增加了代碼的複雜性。使用回調函數也會增加使用全局變量的可能。

2.線程如果發出大量的I/O請求,過段時間它還要處理完成通知,導致其他線程長時間處於空閒狀態,這樣程序的伸縮性不好。

可提醒I/O基礎設施的一個好處:可以進行線程間通信,甚至能夠跨越進程的界限,但是隻能傳遞一個值。實現方法是調用QueueUserAPC()函數後,會向線程的APC隊列添加一項內容,這個內容包含了回調函數和必要的參數。這允許我們讓目標線程執行傳遞到APC的回調函數,就是剛纔說的線程通信。

 

四、I/O完成端口

雖然叫端口,但其實是一個內核對象,不是通信用的端口。I/O完成端口的出現,使得發出I/O請求的線程和處理I/O完成項的線程可以是不同的線程。請記住要想玩轉I/O完成端口,需要這些角色密切配合:


1)發出I/0請求的線程

2)設備對象,線程通過設備驅動程序來讀和寫它們(I/O)

3)I/O完成端口

4)處理I/O完成項的線程池


首先,需要先創建一個I/O完成端口內核對象,並把要訪問的設備的句柄關聯到這個完成端口,這個兩步可以通過一個函數實現:CreateIoCompletionPort()。注意,一個完成端口可以關聯多個不同類型的設備。然後你需要創建一個線程池,用來處理I/O完成項。對於線程池裏面的每一個線程,調用GetQueuedCompletionStatus()是告訴操作系統,我這個線程要處理某個I/O完成端口的完成項,這一步實現了完成端口和處理完成項線程的關聯。該說發出I/O請求的線程了,其實就比較簡單了,一旦有對設備的I/O訪問,因爲設備已經關聯了I/O完成端口,那麼這個I/O請求完成後就會添加到完成端口的I/O完成隊列裏面,等待線程池中的線程進行處理。這一段的流程是困擾了很久才梳理清楚的,這個總體的流程清楚後再去研究I/O完成端口的實現細節相信會輕鬆不少。

本質上,I/O完成端口將發出I/O的線程和處理I/O完成項的分開了,相對於可提醒I/O前進了一步。不僅如此,因爲這種分開,使得每個發出I/O請求的線程的完成項都能得到處理,就不會存在可提醒I/O那種如果一個線程發出大量I/O請求,其他線程將長時間處於空閒的情況了。

還有一個重要特點是,I/O完成端口還能夠設置允許多少線程(這裏是線程池裏的線程)同時處理I/O完成隊列。通常來說,這個數值要小於線程池的數量。也就是說,線程池裏存在等待狀態的線程,這是爲什麼呢?這樣設計的原因是爲了最大程序的保證I/O完成隊列能夠及時得到處理。你想想,處理I/O完成項(從I/O完成隊列獲取)的線程保不準會將自己掛起的,例如wait一個內核對象,這時線程池的等待線程就能馬上補上,確保對I/O完成隊列的處理不會變慢。

另外,類似於可提醒I/O的QueueUserAPC()函數,windows還提供了PostQueuedCompletionStatus()函數。它允許調用者向I/O完成隊列添加一項,想想這可以幹什麼,調用者可以和線程池裏的線程通信。

 

好了,就說這些吧,紙上得來終覺淺,絕知此事要躬行。


這個我的微信公衆號zhixin991,不關注下?


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章