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

C# 高級:TAP 異步編程

開發 后端
我們的應用程序廣泛使用文件和網絡 I/O 操作,I/O 相關 API 傳統上默認是阻塞的,導致用戶體驗和硬件利用率不佳,此類問題的編碼難度也較大。

[[428388]]

本文轉載自微信公眾號「精致碼農」,作者liamwang。轉載本文請聯系精致碼農公眾號。

我們的應用程序廣泛使用文件和網絡 I/O 操作,I/O 相關 API 傳統上默認是阻塞的,導致用戶體驗和硬件利用率不佳,此類問題的編碼難度也較大。

解決此類問題需要使用異步編程,異步強調的是非阻塞,是一種編程模式,主要解決了因文件、網絡等 I/O 操作阻塞主線程工作的問題,比如阻塞期間 UI 無法響應問題。

而異步編程又可以借助多線程技術來解決。前面我們講了基于 System.Threading 命名空間的多線程編程,該命名空間提供的類型是直接和線程相關的 API,雖然可以用來實現異步操作,但有些繁瑣。隨著 .NET 的發展,.NET 對多線程編程相繼做了進一步的抽象封裝,引入了 System.Threading.Tasks 命名空間,使多線程異步編程更簡單易懂。

異步編程主要有如下用途:

在等待 I/O 請求返回的過程中,通過讓出線程使其能處理更多的服務器請求。

在等待 I/O 請求時讓出線程使其繼續進行 UI 交互,并將需要長時間運行的工作過渡到其他 CPU 線程,使用戶界面的響應性更強。

使用 .NET 基于 Task 的異步模型可以直接編寫 I/O 受限和 CPU 受限的異步代碼。該模型圍繞著 Task 和 Task 類型以及 C# 的 async 和 await 關鍵字展開。本文將講解如何使用 .NET 異步編程及一些常見的異步編程操作。

1Task 和 Task

Task 是 Promise 模型的實現。簡單說,它給出“承諾(Promise)”:會在稍后完成工作。而 .NET 的 Task 是為了簡化使用“Promise”而設計的 API。

Task 表示不返回值的操作,Task 表示返回 T 類型的值的操作。

重要的是要把 Task 理解為發起異步工作的抽象,而不是對線程的抽象。默認情況下,Task 在當前線程上執行,并酌情將工作委托給操作系統。可以選擇通過 Task.Run API 明確要求任務在單獨的線程上運行。

Task 提供了一個 API 協議,用于監視、等待和訪問任務的結果值。比如,通過 await 關鍵字等待任務執行完成,為使用 Task 提供了更高層次的抽象。

使用 await 允許你在任務運行期間執行其它有用的工作,將線程的控制權交給其它調用者,直到自己的任務完成。你不再需要依賴回調或事件來在任務完成后繼續執行后續工作。

2Task 的狀態

雖然實際 TAP 編程中很少使用到 Task 的狀態,但它是很多異步操作機理的基礎。Task 類為異步操作提供了一個生命周期,這個周期由 TaskStatus 枚舉表示,它有如下值:

  1. public enum TaskStatus 
  2.     Created = 0, 
  3.     WaitingForActivation = 1, 
  4.     WaitingToRun = 2, 
  5.     Running = 3, 
  6.     WaitingForChildrenToComplete = 4, 
  7.     RanToCompletion = 5, 
  8.     Canceled = 6, 
  9.     Faulted = 7 

其中 Canceled、Faulted 和 RanToCompletion 狀態一起被認為是任務的最終狀態。因此,如果任務處于最終狀態,則其 IsCompleted 屬性為 true 值。

3I/O 受限異步操作

下面示例代碼演示了一個典型的異步 I/O 調用操作:

  1. public Task<string> GetHtmlAsync() 
  2.     // 此處是同步執行 
  3.     var client = new HttpClient(); 
  4.     return client.GetStringAsync("https://www.dotnetfoundation.org"); 

這個例子調用了一個異步方法,并返回了一個活動的 Task,它很可能還沒有完成。

下面第二個代碼示例增加了async和await關鍵字對任務進行操作:

  1. public async Task<string> GetFirstCharactersCountAsync(string url, int count
  2.     // 此處是同步執行 
  3.     var client = new HttpClient(); 
  4.  
  5.     // 此處 await 掛起代碼的執行,把控制權交出去(線程可以去做別的事情) 
  6.     var page = await client.GetStringAsync("https://www.dotnetfoundation.org"); 
  7.  
  8.     // 任務完成后恢復了控制權,繼續執行后續代碼 
  9.     // 此處回到了同步執行 
  10.  
  11.     if (count > page.Length) 
  12.     { 
  13.         return page; 
  14.     } 
  15.     else 
  16.     { 
  17.         return page.Substring(0, count); 
  18.     } 

使用 await 關鍵字告訴當前上下文趕緊生成快照并交出控制權,異步任務執行完成后會帶著返回值去線程池排隊等待可用線程,等到可用線程后,恢復上下文,線程繼續執行后續代碼。

GetStringAsync() 方法的內部通過底層 .NET 庫調用資源(也許會調用其他異步方法),一直到 P/Invoke 互操作調用本地(Native)網絡庫。本地庫隨后可能會調用到一個系統 API(如 Linux 上 Socket 的write()API)。Task 對象將通過層層傳遞,最終返回給初始調用者。

在整個過程中,關鍵的一點是,沒有一個線程是專門用來處理任務的。雖然工作是在某種上下文中執行的(操作系統確實要把數據傳遞給設備驅動程序并中斷響應),但沒有線程專門用來等待請求的數據回返回。這使得系統可以處理更大的工作量,而不是干等著某個 I/O 調用完成。

雖然上面的工作看似很多,但與實際 I/O 工作所需的時間相比,簡直微不足道。用一條不太精確的時間線來表示,大概是這樣的:

  1. 0-1--------------------2-3 

從0到1所花費的時間是await交出控制權之前所花的時間。從1到2花費的時間是GetStringAsync方法花費在 I/O 上的時間,沒有 CPU 成本。最后,從2到3花費的時間是上下文重新獲取控制權后繼續執行的時間。

4CPU 受限異步操作

CPU 受限的異步代碼與 I/O 受限的異步代碼有些不同。因為工作是在 CPU 上完成的,所以沒有辦法繞開專門的線程來進行計算。使用 async 和 await 只是為你提供了一種干凈的方式來與后臺線程進行交互。請注意,這并不能為共享數據提供加鎖保護,如果你正在使用共享數據,仍然需要使用適當的同步策略。

下面是一個 CPU 受限的異步調用:

  1. public async Task<int> CalculateResult(InputData data) 
  2.     // 在線程池排隊獲取線程來處理任務 
  3.     var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data)); 
  4.  
  5.     // 此時此處,你可以并行地處理其它工作 
  6.  
  7.     var result = await expensiveResultTask; 
  8.  
  9.     return result; 

CalculateResult方法在它被調用的線程(一般可以定義為主線程)上執行。當它調用Task.Run時,會在線程池上排隊執行 CPU 受限操作 DoExpensiveCalculation,并接收一個Task句柄。DoExpensiveCalculation會在下一個可用的線程上并行運行,很可能是在另一個 CPU 核上。和 I/O 受限異步調用一樣,一旦遇到await,CalculateResult的控制權就會被交給它的調用者,這樣在DoExpensiveCalculation返回結果的時候,結果就會被安排在主線程上排隊運行。

對于開發者,CPU 受限和 I/O 受限的在調用方式上沒什么區別。區別在于所調用資源性質的不同,不必關心底層對不同資源的調用的具體邏輯。編寫代碼需要考慮的是,對于 CPU 受限的異步任務,根據實際情況考慮是否需要使其和其它任務并行執行,以加快程序的整體運行時間。

5異步編程模式

最后簡單回顧一下 .NET 歷史上提供的三種執行異步操作的模式。

  • 基于任務的異步模式(Task-based Asynchronous Pattern,TAP),它使用單一的方法來表示異步操作的啟動和完成。TAP 是在 .NET Framework 4 中引入的。它是 .NET 中異步編程的推薦方法。C# 中的 async 和 await 關鍵字為 TAP 添加了語言支持。
  • 基于事件的異步模式(Event-based Asynchronous Pattern,EAP),這是基于事件的傳統模式,用于提供異步行為。它需要一個具有 Async 后綴的方法和一個或多個事件。EAP 是在 .NET Framework 2.0 中引入的。它不再被推薦用于新的開發。
  • 異步編程模式(Asynchronous Programming Model,APM)模式,也稱為 IAsyncResult 模式,這是使用 IAsyncResult 接口提供異步行為的傳統模式。在這種模式中,需要Begin和End方法同步操作(例如,BeginWrite和EndWrite來實現異步寫操作)。這種模式也不再推薦用于新的開發。

下面簡單舉例對三種模式進行比較。

假設有一個 Read 方法,該方法從指定的偏移量開始將指定數量的數據讀入提供的緩沖區:

  1. public class MyClass 
  2.     public int Read(byte [] buffer, int offset, int count); 

若用 TAP 異步模式來改寫,該方法將是簡單的一個 ReadAsync 方法:

  1. public class MyClass 
  2.     public Task<int> ReadAsync(byte [] buffer, int offset, int count); 

若使用 EAP 異步模式,需要額外多定義一些類型和成員:

  1. public class MyClass 
  2.     public void ReadAsync(byte [] buffer, int offset, int count); 
  3.     public event ReadCompletedEventHandler ReadCompleted; 
  4.  
  5. public delegate void ReadCompletedEventHandler( 
  6.     object sender, ReadCompletedEventArgs e); 
  7.  
  8. public class ReadCompletedEventArgs : AsyncCompletedEventArgs 
  9.     public MyReturnType Result { get; } 

若使用 AMP 異步模式,則需要定義兩個方法,一個用于開始執行異步操作,一個用于接收異步操作結果:

  1. public class MyClass 
  2.     public IAsyncResult BeginRead( 
  3.         byte [] buffer, int offset, int count
  4.         AsyncCallback callback, object state); 
  5.     public int EndRead(IAsyncResult asyncResult); 

后兩種異步模式已經過時不推薦使用了,這里也不再繼續探討。年長的 .NET 程序員可能比較熟悉后兩種異步模式,畢竟那時候沒有 async/await,應該沒少折騰。

下面來介紹幾個常見的基于 TAP 的異步操作。

6手動控制任務啟動

為了支持手動控制任務啟動,并支持構造與調用的分離,Task 類提供了一個 Start 方法。由 Task 構造函數創建的任務被稱為冷任務,因為它們的生命周期處于 Created 狀態,只有該實例的 Start 方法被調用才會啟動。

任務狀態平時用的情況不多,一般我們在封裝一個任務相關的方法時,可能會用到。比如下面這個例子,需要判斷某任務滿足一定條件才啟動:

  1. static void Main(string[] args) 
  2.     MyTask t = new(() => 
  3.     { 
  4.         // do something. 
  5.     }); 
  6.  
  7.     StartMyTask(t); 
  8.  
  9.     Console.ReadKey(); 
  10.  
  11. public static void StartMyTask(MyTask t) 
  12.     if (t.Status == TaskStatus.Created && t.Counter>10) 
  13.     { 
  14.         t.Start(); 
  15.     } 
  16.     else 
  17.     { 
  18.         // 這里模擬計數業務代碼,直到 Counter>10 再執行 Start 
  19.         while (t.Counter <= 10) 
  20.         { 
  21.             // Do something 
  22.             t.Counter++; 
  23.         } 
  24.         t.Start(); 
  25.     } 
  26.  
  27. public class MyTask : Task 
  28.     public MyTask(Action action) : base(action
  29.     { 
  30.     } 
  31.  
  32.     public int Counter { get; set; } 

同樣,TaskStatus.Created 狀態以外的狀態,我們叫它熱任務,熱任務一定是被調用了 Start 方法激活過的。

7確保任務已激活

注意,所有從 TAP 方法返回的任務都必須被激活,比如下面這樣的代碼:

  1. MyTask task = new(() => 
  2.     Console.WriteLine("Do something."); 
  3. }); 
  4.  
  5. // 在其它地方調用 
  6. await task; 

在 await 之前,任務沒有執行 Task.Start 激活,await 時程序就會一直等待下去。所以如果一個 TAP 方法內部使用 Task 構造函數來實例化要返回的 Task,那么 TAP 方法必須在返回 Task 對象之前對其調用 Start。

8任務取消

在 TAP 中,取消對于異步方法實現者和消費者來說都是可選的。如果一個操作允許取消,它就會暴露一個異步方法的重載,該方法接受一個取消令牌(CancellationToken 實例)。按照慣例,參數被命名為 cancellationToken。例如:

  1. public Task ReadAsync( 
  2.     byte [] buffer, int offset, int count
  3.     CancellationToken cancellationToken) 

異步操作會監控這個令牌是否有取消請求。如果收到取消請求,它可以選擇取消操作,如下面的示例通過 while 來監控令牌的取消請求:

  1. static void Main(string[] args) 
  2.     CancellationTokenSource source = new(); 
  3.     CancellationToken token = source.Token; 
  4.  
  5.     var task = DoWork(token); 
  6.  
  7.     // 實際情況可能是在稍后的其它線程請求取消 
  8.     Thread.Sleep(100); 
  9.     source.Cancel(); 
  10.  
  11.     Console.WriteLine($"取消后任務返回的狀態:{task.Status}"); 
  12.  
  13.     Console.ReadKey(); 
  14.  
  15. public static Task DoWork(CancellationToken cancellationToken) 
  16.     while (!cancellationToken.IsCancellationRequested) 
  17.     { 
  18.         // Do something. 
  19.         Thread.Sleep(1000); 
  20.  
  21.         return Task.CompletedTask; 
  22.     } 
  23.     return Task.FromCanceled(cancellationToken); 

如果取消請求導致工作提前結束,甚至還沒有開始就收到請求取消,則 TAP 方法返回一個以 Canceled 狀態結束的任務,它的 IsCompleted 屬性為 true,且不會拋出異常。當任務在 Canceled 狀態下完成時,任何在該任務注冊的延續任務仍都會被調用和執行,除非指定了諸如 NotOnCanceled 這樣的選項來選擇不延續。

但是,如果在異步任務在工作時收到取消請求,異步操作也可以選擇不立刻結束,而是等當前正在執行的工作完成后再結束,并返回 RanToCompletion 狀態的任務;也可以終止當前工作并強制結束,根據實際業務情況和是否生產異常結果返回 Canceled 或 Faulted 狀態。

對于不能被取消的業務方法,不要提供接受取消令牌的重載,這有助于向調用者表明目標方法是否可以取消。

9進度報告

幾乎所有異步操作都可以提供進度通知,這些通知通常用于用異步操作的進度信息更新用戶界面。

在 TAP 中,進度是通過 IProgress 接口來處理的,該接口作為一個參數傳遞給異步方法。下面是一個典型的的使用示例:

  1. static void Main(string[] args) 
  2.     var progress = new Progress<int>(n => 
  3.     { 
  4.         Console.WriteLine($"當前進度:{n}%"); 
  5.     }); 
  6.  
  7.     var task = DoWork(progress); 
  8.  
  9.     Console.ReadKey(); 
  10.  
  11. public static async Task DoWork(IProgress<int> progress) 
  12.     for (int i = 1; i <= 100; i++) 
  13.     { 
  14.         await Task.Delay(100); 
  15.         if (i % 10 == 0) 
  16.         { 
  17.             progress?.Report(i); 
  18.         }; 
  19.     } 

輸出如下結果:

  1. 當前進度:10% 
  2. 當前進度:20% 
  3. 當前進度:30% 
  4. 當前進度:40% 
  5. 當前進度:50% 
  6. 當前進度:60% 
  7. 當前進度:70% 
  8. 當前進度:80% 
  9. 當前進度:90% 
  10. 當前進度:100% 

IProgress 接口支持不同的進度實現,這是由消費代碼決定的。例如,消費代碼可能只關心最新的進度更新,或者希望緩沖所有更新,或者希望為每個更新調用一個操作,等等。所有這些選項都可以通過使用該接口來實現,并根據特定消費者的需求進行定制。例如,如果本文前面的 ReadAsync 方法能夠以當前讀取的字節數的形式報告進度,那么進度回調可以是一個 IProgress 接口。

  1. public Task ReadAsync( 
  2.     byte[] buffer, int offset, int count
  3.     IProgress<long> progress) 

再如 FindFilesAsync 方法返回符合特定搜索模式的所有文件列表,進度回調可以提供工作完成的百分比和當前部分結果集,它可以用一個元組來提供這個信息。

  1. public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( 
  2.     string pattern, 
  3.     IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress) 

或使用 API 特有的數據類型:

  1. public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( 
  2.     string pattern, 
  3.     IProgress<FindFilesProgressInfo> progress) 

如果 TAP 的實現提供了接受 IProgress 參數的重載,它們必須允許參數為空,在這種情況下,不會報告進度。IProgress 實例可以作為獨立的對象,允許調用者決定如何以及在哪里處理這些進度信息。

10Task.Yield 讓步

我們先來看一段 Task.Yield() 的代碼:

  1. Task.Run(async () => 
  2.     for(int i=0; i<10; i++) 
  3.     { 
  4.         await Task.Yield(); 
  5.         ... 
  6.     } 
  7. }); 

這里的 Task.Yield() 其實什么也沒干,它返回的是一個空任務。那 await 一個什么也沒做的空任務有什么用呢?

我們知道,對計算機來說,任務調度是根據一定的優先策略來安排線程去執行的。如果任務太多,線程不夠用,任務就會進入排隊狀態。而 Yield 的作用就是讓出等待的位置,讓后面排除的任務先行。它字面上的意思就是讓步,當任務做出讓步時,其它任務就可以盡快被分配線程去執行。舉個現實生活中的例子,就像你在排隊辦理業務時,好不容易到你了,但你的事情并不急,自愿讓出位置,讓其他人先辦理,自己假裝臨時有事到外面溜一圈什么事也沒干又回來重新排隊。默默地做了一次大善人。

Task.Yield() 方法就是在異步方法中引入一個讓步點。當代碼執行到讓步點時,就會讓出控制權,去線程池外面兜一圈什么事也沒干再回來重新排隊。

11定制異步任務后續操作

我們可以對異步任務執行完成的后續操作進行定制。常見的兩個方法是 ConfigureAwait 和 ContinueWith。

ConfigureAwait

我們先來看一段 Windows Form 中的代碼:

  1. private void button1_Click(object sender, EventArgs e) 
  2.     var content = CurlAsync().Result; 
  3.     ... 
  4.  
  5. private async Task<string> CurlAsync() 
  6.     using (var client = new HttpClient()) 
  7.     { 
  8.         return  await client.GetStringAsync("http://geekgist.com"); 
  9.     } 

想必大家都知道 CurlAsync().Result 這句代碼在 Windows Form 程序中會造成死鎖。原因是 UI 主線程執行到這句代碼時,就開始等待異步任務的結果,處于阻塞狀態。而異步任務執行完后回來準備找 UI 線程繼續執行后面的代碼時,卻發現 UI 線程一直處于“忙碌”的狀態,沒空搭理回來的異步任務。這就造成了你等我,我又在等你的尷尬局面。

當然,這種死鎖的情況只會在 Winform 和早期的 ASP.NET WebForm 中才會發生,在 Console 和 Web API 應用中不會生產死鎖。

解決辦法很簡單,作為異步方法調用者,我們只需改用 await 即可:

  1. private async void button1_Click(object sender, EventArgs e) 
  2.     var content = await CurlAsync(); 
  3.     ... 

在異步方法內部,我們也可以調用任務的 ConfigureAwait(false) 方法來解決這個問題。如:

  1. private async Task<string> CurlAsync() 
  2.     using (var client = new HttpClient()) 
  3.     { 
  4.         return  await client 
  5.             .GetStringAsync("http://geekgist.com"
  6.             .ConfigureAwait(false); 
  7.     } 

雖然兩種方法都可行,但如果作為異步方法提供者,比如封裝一個通用庫時,考慮到難免會有新手開發者會使用 CurlAsync().Result,為了提高通用庫的容錯性,我們就可能需要使用 ConfigureAwait 來做兼容。

ConfigureAwait(false) 的作用是告訴主線程,我要去遠行了,你去做其它事情吧,不用等我。只要先確保一方不在一直等另一方,就能避免互相等待而造成死鎖的情況。

ContinueWith

ContinueWith 方法很容易理解,就是字面上的意思。作用是在異步任務執行完成后,安排后續要執行的工作。示例代碼:

  1. private void Button1_Click(object sender, EventArgs e) 
  2.     var backgroundScheduler = TaskScheduler.Default
  3.     var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); 
  4.     Task.Factory 
  5.         .StartNew(_ => DoBackgroundComputation(), backgroundScheduler) 
  6.         .ContinueWith(_ => UpdateUI(), uiScheduler) 
  7.         .ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler) 
  8.         .ContinueWith(_ => UpdateUIAgain(), uiScheduler); 

如上,可以一直鏈式的寫下去,任務會按照順序執行,一個執行完再繼續執行下一個。若其中一個任務返回的狀態是 Canceled 時,后續的任務也將被取消。這個方法有好些個重載,在實際用到的時候再查看文檔即可。

12小結

System.Threading.Tasks 命名空間中關鍵的一個類是 Task 類,基于 Task 的異步 API 和語言級異步編程模式顛覆了傳統模式,使得異步編程非常簡單。它使我們可以只關注業務層面要處理的任務,而不必關心和使用線程或線程池。重要的是要把 Task 理解為發起異步工作的抽象,而不是對線程的抽象。本文還介紹了 .NET 異步編程模式,而我們現在主流用的都是 TAP 模式,最后本文羅列一些常見的異步操作。

 

責任編輯:武曉燕 來源: 精致碼農
相關推薦

2015-09-16 15:11:58

C#異步編程

2009-08-20 17:30:56

C#異步編程模式

2012-07-27 10:02:39

C#

2024-10-15 08:29:09

C#軟件開發

2016-12-14 15:05:08

C#異步編程

2009-08-20 17:47:54

C#異步編程模式

2025-04-30 01:50:00

C#異步編程

2009-08-21 10:17:14

C#異步網絡編程

2009-08-17 08:04:00

C#高級編程

2009-08-17 13:34:02

C#異步操作

2009-08-03 16:45:02

C#異步Socket

2024-12-23 09:09:54

2024-05-11 07:13:33

C#Task編程

2025-01-09 07:54:03

2015-06-29 10:05:10

C#異步編程解析

2024-06-25 08:33:48

2009-08-24 11:02:52

C#接口映射

2009-08-26 10:34:15

C#類型C#變量

2009-08-24 09:55:26

C#接口轉換

2013-05-16 10:33:11

C#C# 5.0Async
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 男人天堂视频在线观看 | 精品毛片在线观看 | 亚洲精品久久久一区二区三区 | 午夜影院在线观看 | 好好的日在线视频 | 91视频在线 | 精品亚洲一区二区三区四区五区高 | 色婷婷激情 | 日韩av视屏| 午夜av免费| 国产精品一区一区 | 天堂视频一区 | 日韩精品视频在线 | 免费99视频 | 欧美视频三区 | 秋霞精品| 国产不卡视频在线 | 日本三级全黄三级a | 一级片网址 | 国产精品视频一二三区 | 欧美三级在线 | 欧美国产一区二区三区 | 久久小视频| 自拍偷拍精品 | 久久99蜜桃综合影院免费观看 | 一a级片 | 久久国产精品一区二区三区 | 99精品在线免费观看 | 一区二区片 | 国产午夜视频 | 国产成人综合一区二区三区 | 国产一区亚洲 | 国产一级精品毛片 | 成人av在线播放 | 伊人免费观看视频 | 欧州一区二区三区 | 精品久久久久久久久久 | 亚洲一区不卡在线 | 亚洲+变态+欧美+另类+精品 | 在线小视频 | 日韩成人 |