編程
windows是如何執行I/O操做的?計算機的每一個模塊都有本身的微型處理器,當寫文件到磁盤中時,操做系統將寫文件的任務交給磁盤的處理單元就能夠作其餘的了。還有須要TCP/IP 與另外一臺電腦通訊時,系統只要將發送的數據寫入TCP的緩存區就能夠作其餘的了,發送數據由網卡處理單元完成。windows
可是這些模塊的處理單元的計算能力遠不如CPU快,若是將CPU的計算資源老是和這些模塊的處理單元同步的話,就會影響應用程序的性能,給用戶帶來很差的體驗。這一節就講解如何執行計算限制的操做,利用線程池在多個CPU 內核上調度任務,使多個線程併發工做,從而高效使用系統資源。緩存
1 Windows 如何執行I/O 操做安全
用一個從磁盤中讀取文件中的數據爲例:服務器
1 程序經過構造一個 FileStream 對象來打開磁盤文件,而後調用 Read 方法從文件讀取數據。網絡
2 調用 FileStream 的Read 方法時,你的線程從託管代碼轉變爲本機/用戶模式代碼,Read 內部調用 Win32 ReadFile 函數。數據結構
3 ReadFile 分配一個小的數據結構,稱爲 I/O 請求包 (I/O Request Packet,IRP)。 而後 ReadFile 將你的線程從本機/ 用戶模式代碼轉變成本機/內核模式代碼,向內核傳遞 IRP 數據結構,從而調用 Windows 內核。併發
4 根據 IRP 中的設備句柄, Windows 內核知道I/O 操做要傳送給哪一個硬件設備。所以,Windows 將IRP 傳送給恰當的設備驅動程序的IRP 隊列。異步
5 每一個設備驅動程序都維護着本身的 IRP 隊列,其中包含了機器上運行的全部進程發出的I/O 請求。IRP 數據包到達時,設備驅動程序將IRP 信息傳遞給物理硬件設備上安裝的電路板。如今,硬件設備將執行這些請求的I/O 操做。async
6 在硬件設備執行I/O 操做期間,發出了I/O 請求的線程將無事可作,因此 Windows 將線程變成睡眠狀態,防止浪費CPU 時間。
7 最終,硬件設備會完成 I/O 操做。而後,Windows 會喚醒你的線程,把它調度給一個 CPU ,使它從內核模式返回用戶模式,再返回至託管代碼。
如今討論一下 Windows 如何執行異步I/O 操做。引入了 CLR 的線程池。打開磁盤文件的方式任然是經過構造一個 FileStream 對象,但如今傳遞了一個 FileOptions.Asynchronous 標誌。告訴Windows 我但願文件的讀/寫 操做以異步方式執行。
1 如今調用 ReadAsync 而不是 Read 從文件中讀取數據。ReadAsync 內部分配一個 Task<Int32> 對象來表明用於完成讀取操做的代碼。而後,ReadAsync 調用 Win32 ReadFile函數。
2 ReadFile 分配 IRP 添加到硬盤驅動程序的 IRP 隊列中。但線程再也不阻塞,而是容許返回至你的代碼。
3 那何時以及什麼方式處理最終讀取的數據呢?
注意:調用 ReadAsync 返回的是一個 Task<Int32> 對象,可在該對象調用 ContinueWith 來登記任務完成時執行的回調方法。也能夠用C# 的異步函數功能簡化編碼。
4 硬件設備處理好IRP 後,會將完成的 IRP 放到 CLR 的線程池隊列中。未來的某個時候,一個線程池線程會提取完成的 IRP 並執行完成任務的代碼,最終要麼設置異常,要麼返回結果。這樣一來,Task 對象就知道操做在何時完成,代碼能夠開始運行並安全地訪問 Byte[] 中的數據。
CLR 的線程池使用名爲 「I/O完成端口」(I/O Completion Port)的 Windows 資源來引出我剛纔描述的行爲。CLR 在初始化時建立一個 I/O 完成端口。當你打開硬件設備時,這些設備能夠和I/O 完成端口關聯,使設備驅動程序知道完成的IRP 送到哪。
以異步方式執行 I/O 操做有不少好處:
1 將資源利用率降到最低,並減小上下文切換。
2 每開始一次垃圾回收,CLR 都會掛起進程中的全部線程。因此,線程越少,垃圾回收器運行的速度越快。
3 垃圾回收時,CLR 遍歷全部線程棧來查找根,一樣線程越少,棧的數量越少,使垃圾回收速率變得更快。
C# 的異步函數
Microsoft 設計了一個編程模型來幫助開發者利用這種異步操做能力。該模式利用了上一章的 Task 和 稱爲 異步函數 的一個C# 語言功能。如下代碼使用異步函數來執行兩個異步 I/O 操做。
private static async Task<String> IssueClientRequestAsync(String serverName, String message) {
using (var pipe = new NamedPipeClientStream(serverName, "PipeName", PipeDirection.InOut,PipeOptions.Asynchronous | PipeOptions.WriteThrough)) {
pipe.Connect(); // Must Connect before setting ReadMode
pipe.ReadMode = PipeTransmissionMode.Message;
// Asynchronously send data to the server
Byte[] request = Encoding.UTF8.GetBytes(message);
await pipe.WriteAsync(request, 0, request.Length);
// Asynchronously read the server's response
Byte[] response = new Byte[1000];
Int32 bytesRead = await pipe.ReadAsync(response, 0, response.Length);
return Encoding.UTF8.GetString(response, 0, bytesRead);
} // Close the pipe
}
下面來解釋一下上述代碼中的異步函數執行過程。對於理解await 很是重要。
方法標記爲 async ,編譯器就會將方法的代碼轉換成實現了狀態機的一個類型。這就容許線程執行狀態機中的一些代碼並返回,方法不須要一直執行到結束。
調用WriteAsync時,在WriteAsync 內部分配了一個 Task 對象並把它返回給IssueClientRequestAsync ,此時,C# await 操做符實際會在 Task 對象上調用ContinueWith ,向它傳遞用於恢復狀態機的方法。而後線程從 IssueClientRequestAsync返回。
在未來某個時候,網絡設備驅動程序會結束向管道的寫入,一個線程池線程會通知Task 對象,後者激活ContinueWith回調方法,形成一個線程恢復狀態機。更具體的說,一個線程會從新進入 IssueClientRequestAsync 方法,但此次是從 await 操做符的位置開始的。
方法如今執行編譯器生成的、用於查詢Task 對象狀態的代碼。若是操做失敗,會設置表明錯誤的一個異常。若是操做成功完成,await 操做符會返回結果。本列子中,WriteAsync 返回一個Task 而不是Task<TResult>, 因此無返回值。
如今方法繼續執行,分配一個Byte[] 並調用 NamedPipeClientStream 的異步 ReadAsync 方法。ReadAsync 內部建立一個 Task<Int32>對象並返回它。一樣的,await 操做符實際會在Task<Int32>對象上調用 ContinueWith,向其傳遞用於恢復狀態機的方法。而後線程再次從 IssueClientRequestAsync 返回。
未來的某個時候,服務器向客戶機發送一個響應,網絡設備驅動程序得到這個響應,一個線程池線程通知 Task<Int32>對象,後者恢復狀態機。await 操做符形成編譯器生成代碼來查詢 Task對象的Result 屬性(一個 Int32)並將結果賦給局部變量 bytesRead。若是操做失敗,則拋出異常。而後執行 IssueClientRequestAsync 剩餘的代碼,返回結果字符串並關閉管道。此時狀態機執行完畢,垃圾回收器會回收任何內存。
調用者如何知道 IssueClientRequestAsync 已經執行完畢它的狀態機呢?一旦將方法標記爲 async,編譯器會自動生成代碼,在狀態機開始執行時建立一個 Task 對象。該Task 對象在狀態機執行完畢時自動完成。注意 IssueClientRequestAsync 方法的返回類型是 Task<String>,它實際返回的是由編譯器生成的代碼爲這個方法(IssueClientRequestAsync 方法)的調用者而建立的Task<String> 對象,Task 的Result 屬性在本列中是 String 類型。在IssueClientRequestAsync 方法靠近尾部的地方,我反回了一個字符串。這形成編譯器生成的代碼完成它建立的 Task<String>對象,把對象的Result 屬性設爲返回的字符串。
注意:異步函數存在如下限制。
1 不能轉變爲異步函數的狀況,Main方法、構造器、屬性訪問器方法和 事件訪問器方法。
2 異步函數不能使用任何 out 或 ref 參數。
3 不能在 catch ,finally 或 unsafe 塊中使用 await 操做符。
4 不能在 await 操做符以前得到一個支持線程全部權 或遞歸的鎖,並在 await 操做符以後釋放它。 由於 await 以前的代碼由一個線程執行,而await 以後的代碼由另外一個線程執行。
5 在查詢表達式中,await 操做符只能在初始 from 子句的第一個集合表達式中使用,或者在 join 子句的集合表達式中使用。
異步函數擴展性
在擴展性方面,用Task對象包裝一個未來完成的操做,就能夠用await 操做符來等待該操做。下面是Jeffrey Richter 寫的一個TaskLogger 類,它能夠顯示還沒有完成的異步操做。咱們能夠在調試的時候使用。
public static class TaskLogger {
public enum TaskLogLevel { None, Pending }
public static TaskLogLevel LogLevel { get; set; }
public sealed class TaskLogEntry {
public Task Task { get; internal set; }
public String Tag { get; internal set; }
public DateTime LogTime { get; internal set; }
public String CallerMemberName { get; internal set; }
public String CallerFilePath { get; internal set; }
public Int32 CallerLineNumber { get; internal set; }
public override string ToString() {
return String.Format("LogTime={0}, Tag={1}, Member={2}, File={3}({4})",
LogTime, Tag ?? "(none)", CallerMemberName, CallerFilePath, CallerLineNumber);
}
}
private static readonly ConcurrentDictionary<Task,TaskLogEntry> s_log =
new ConcurrentDictionary<Task, TaskLogEntry>();
public static IEnumerable<TaskLogEntry> GetLogEntries() { return s_log.Values; }
public static Task<TResult> Log<TResult>(this Task<TResult> task, String tag = null,
[CallerMemberName] String callerMemberName = null,
[CallerFilePath] String callerFilePath = null,
[CallerLineNumber] Int32 callerLineNumber = -1) {
return (Task<TResult>)
Log((Task)task, tag, callerMemberName, callerFilePath, callerLineNumber);
}
public static Task Log(this Task task, String tag = null,
[CallerMemberName] String callerMemberName = null,
[CallerFilePath] String callerFilePath = null,
[CallerLineNumber] Int32 callerLineNumber = •1) {
if (LogLevel == TaskLogLevel.None) return task;
var logEntry = new TaskLogEntry {
Task = task,
LogTime = DateTime.Now,
Tag = tag,
CallerMemberName = callerMemberName,
CallerFilePath = callerFilePath,
CallerLineNumber = callerLineNumber
};
s_log[task] = logEntry;
task.ContinueWith(t