程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 更多關於編程 >> C#:異步編程和線程的使用(.NET 4.5 )

C#:異步編程和線程的使用(.NET 4.5 )

編輯:更多關於編程

      異步編程和線程處理是並發或並行編程非常重要的功能特征。為了實現異步編程,可使用線程也可以不用。將異步與線程同時講,將有助於我們更好的理解它們的特征。

        本文中涉及關鍵知識點
        1. 異步編程   2. 線程的使用   3. 基於任務的異步模式   4. 並行編程   5. 總結
        異步編程
        什麼是異步操作?異步操作是指某些操作能夠獨立運行,不依賴主流程或主其他處理流程。通常情況下,C#程序從Main方法開始,當Main方法返回時結束。所有的操作都是按順序執行的。執行操作是有序列的,一個操作必須等到其前面的操作完成才能夠執行。如以下代碼示例:
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  DoTaskOne();   6:   7:  DoTaskTwo();   8:   9:  }   “DoTaskOne”方法結束後,DoTaskTwo()才能夠執行。
        異步編程中常用後台運行的方法體現,主調用線程不會被阻塞。調用後台運行的方法後,執行流程會立即返回到調用的線程並繼續執行其他任務。後台運行方法通常是用線程或任務來實現。
        在上面的例子中,在“DoTaskOne”方法調用成功後,如果“DoTaskOne”是異步調用,,執行流程立即返回到Main方法中,並繼續執行“DoTaskTwo” 方法。   C#提供了Thread類創建線程實現異步編程,或者使用.NET提供的異步模式實現異步編程。.NET中提供了三種不同的異步模式:
        1. 異步編程模型(APM)模式   2. 基於事件的異步模式(EAP)   3. 基於任務的異步模式(TAP)
        前兩種模型微軟官方並不推薦使用,本文不再詳細描述。我們將詳細討論基於任務的異步模式(TAP):
        線程的使用
        在.NET 4.5中引入了異步編程模式,大部分情況下都不需要我們手動創建線程。編譯器已經替代了開發人員來完成這項工作。
        創建新線程是非常耗時的。一般情況下,異步和並行編程使用 “基於任務的異步模式(TAP)”和“任務並行庫(TPL)”就夠了。如果需要控制線程的功能則需要使用其他模式。
        TAP和TPL都是基於任務。一般來說任務是從線程池中調用線程( 線程池是.NET框架創建的和維護的線程集。如果我們使用任務,就不需要直接調用線程池。
        任務可以在以下情況運行:
        1. 在正在運行的線程中   2. 在新線程中   3. 從線程池中的某一線程中   4. 沒有線程也可以運行
        如果使用任務機制,開發人員就不必擔心線程的創建或使用,.NET框架已經為我們解決了這一難題。
        有時候需要控制線程,執行以下操作:
        1. 設置線程名稱   2. 設置線程優先級   3. 設置線程是前端或後端運行
        我們可以使用線程類來創建線程。
        使用Thread類創建線程
        Thread類的構造函數接收委托類型的參數
        1. ThreadStart:定義了返回值為空的方法,且不帶參數的方法。   2. ParameterizedThreadStart:定義了返回值為空且有一個object類型的參數。
        下面是一個簡單的例子,使用 Start方法啟動一個新線程:
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  Thread thread = new Thread(DoTask);   6:   7:  thread.Start();// Start DoTask method in a new thread   8:   9:  //Do other tasks in main thread   10:   11:  }   12:   13:  static public void DoTask() {   14:   15:  //do something in a new thread   16:   17:  }
        可以用Lamda表達式代替線程名稱:
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  Thread thread = new Thread(() => {   6:   7:  //do something in a new thread   8:   9:  });   10:   11:  thread.Start();// Start a new thread   12:   13:  //Do other tasks in main thread   14:   15:  }
        如果不需要引用變量,可如下直接啟動線程:
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  new Thread(() => {   6:   7:  //do something in a new thread   8:   9:  }).Start();// Start a new thread   10:   11:  //Do other tasks in main thread   12:   13:  }
        但是,如果想控制線程對象,對線程設置一些屬性,需要在線程創建後引用線程變量。如下可給線程對象的不同屬性設值:
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  Thread thread = new Thread(DoTask);   6:   7:  thread.Name = "My new thread";// Asigning name to the thread   8:   9:  thread.IsBackground = false;// Made the thread forground   10:   11:  thread.Priority = ThreadPriority.AboveNormal;// Setting thread priority   12:   13:  thread.Start();// Start DoTask method in a new thread   14:   15:  //Do other task in main thread   16:   17:  }
        調用引用變量,可以執行一些操作如中止線程或通過調用join方法等待阻塞線程。
        如果需要通過函數傳值,可以給Start方法傳值。由於該方法的參數為Object類型,因此需要強制轉換類型。
        1:  static void Main(string[] args)   2:   3:  {   4:   5:  Thread thread = new Thread(DoTaskWithParm);   6:   7:  thread.Start("Passing string");// Start DoTaskWithParm method in a new thread   8:   9:  //Do other task in main thread   10:   11:  }   12:   13:  static public void DoTaskWithParm(object data)   14:   15:  {   16:   17:  //we need to cast the data to appropriate object   18:   19:  }
        “async”和“await”關鍵字
        .NET框架引入了兩個新的關鍵字來實現異步編程:“async”和“await”。使用 “await”的異步方法必須由“async”修飾符來聲明方法。“await”關鍵字修飾調用異步方法。await 運算符應用於一個異步方法中的任務以掛起該方法的執行,直到等待任務完成.如下:
        1:  private async static void CallerWithAsync()// async modifier is used   2:   3:  {   4:   5:  string result = await GetSomethingAsync();// await is used before a method call. It suspends   6:     //execution of CallerWithAsync() method and control returs to the calling thread that can   //perform other task.   7:   8:  Console.WriteLine(result);   9:     // this line would not be executed before GetSomethingAsync() //method completes   10:   11:  }
        而“ async ”修飾符只能用於返回值為Task類型或Void的方法。它不能用於主程序的切入點。
        所有的方法之前不能使用await關鍵字,使用“await”關鍵字方法必須返回 “可等待”類型。以下屬於“可等待”類型:
        1. Task   2. Task<T>   3. 自定義“可等待”類型。
        基於任務的異步模式
        首先我們需要聲明一個返回類型為Task或Task<T>的異步方法。可以通過以下幾種方式創建任務:
        1. Task.Factory.StartNew方法:在之前的.NET版本(在.NET 4中),是創建和啟動任務的主要方法。   2. Task.Run或Task.Run <T>方法:從.NET 4.5這個方法已經被使用。此方法足以滿足常見情況。   3. Task.FromResult方法:如果結果是已計算,就可以用這個方法來創建任務。
        創建並等待一個任務
        使用Task.Run <T>方法創建Task。該方法將特定工作按順序排列在線程池中運行,並返回工作的任務句柄。需要以下步驟從同步方法中創建異步任務:
        1. 假設下面方法是同步的,但需要一定的時間來完成:   1:  static string Greeting(string name)   2:   3:  {   4:   5:  Thread.Sleep(3000);   6:   7:  return string.Format("Hello, {0}", name);   8:   9:  }   2. 要以異步方式訪問此方法,必須以異步方式封裝。命名為“GreetingAsync”。增加“Async”的後綴命名異步方法。   1:  static Task<string> GreetingAsync(string name)   2:   3:  {   4:   5:  return Task.Run<string>(() =>   6:   7:  {   8:   9:  return Greeting(name);   10:   11:  });   12:   13:  }   3.現在,可通過使用的await關鍵字調用異步方法GreetingAsync   1:  private async static void CallWithAsync()   2:   3:  {   4:   5:  //some other tasks   6:   7:  string result = await GreetingAsync("Bulbul");   8:   9:  //We can add multiple “await” in same “async” method   10:   11:  //string result1 = await GreetingAsync(“Ahmed”);   12:   13:  //string result2 = await GreetingAsync(“Every Body”);   14:   15:  Console.WriteLine(result);   16:   17:  }
        當“CallWithAsync”方法被調用時,與常規的同步方法一樣執行,直到遇到“await”的關鍵字。當它執行到 await的關鍵字會處理執行,並開始等待“GreetingAsync(” Bulbul “)” 方法被完成。同時,程序流將返回” CallWithAsync “方法的調用者,並繼續執行調用者的任務。
        當“GreetingAsync(" Bulbul ") 方法完成,“CallWithAsync”的方法恢復 “await關鍵字後的其他任務。在本實例中,將繼續執行的代碼“Console.WriteLine(result)”
        4. 使用任務持續:Task類 “ContinueWith”的方法定義了Task完成後被調用的代碼。   1:  private static void CallWithContinuationTask()   2:   3:  {   4:   5:  Task<string> t1 = GreetingAsync("Bulbul");   6:   7:  t1.ContinueWith(t =>   8:   9:  {   10:   11:  string result = t.Result;   12:   13:  Console.WriteLine(result);   14:   15:  });   16:   17:  }
        如果使用“ContinueWith”的方法就不需要使用“await“關鍵字,編譯器會自動在合適的位置中添加“await”關鍵字。
        等候多??個異步方法。
        看看下面的代碼:
        1:  private async static void CallWithAsync()   2:   3:  {   4:   5:  string result = await GreetingAsync("Bulbul");   6:   7:  string result1 = await GreetingAsync(&ldquo;Ahmed&rdquo;);   8:   9:  Console.WriteLine(result);   10:   11:  Console.WriteLine(result1);   12:   13:  }
        有兩個正在等待調用函數序列。“GreetingAsync(” Ahmed “)” 會在完成第一個呼叫“GreetingAsync(” Bulbul “)” 之後啟動。如果“result”和上面的代碼“result1”是獨立的,那麼連續的“awiating”並不是一個好的做法。
        在這種情況下,我們可以簡化調用方法,不需要添加多個“await”關鍵字,只在一個地方添加await關鍵字,如下所示,這種情況下,該方法的調用都可以並行執行。
        1:  private async static void MultipleAsyncMethodsWithCombinators()   2:   3:  {   4:   5:  Task<string> t1 = GreetingAsync("Bulbul");   6:   7:  Task<string> t2 = GreetingAsync("Ahmed");   8:   9:  await Task.WhenAll(t1, t2);   10:   11:  Console.WriteLine("Finished both methods.n " +   12:   13:  "Result 1: {0}n Result 2: {1}", t1.Result, t2.Result);   14:   15:  }
        在這裡,我們使用Task.WhenAll連接器。Task.WhenAll創建一個任務,將完成所有的提供的任務。Task類也有其他的結合器。Task.WhenAny,當所任務鏈中所有的任務完成時,結束使用。
        處理異常
        必須把“await的代碼塊放在try塊內捕獲異常。   1:  private async static void CallWithAsync()   2:   3:  {   4:   5:  try   6:   7:  {   8:   9:  string result = await GreetingAsync("Bulbul");   10:   11:  }   12:   13:  catch (Exception ex)   14:   15:  {   16:   17:  Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);   18:   19:  }   20:   21:  }
        如果try塊中有多個“await”,只有第一個” await“異常會被處理,其他“await”將無法被捕捉。如果希望所有的方法都能捕獲異常,不能使用“await”關鍵字調用方法,使用Task.WhenAll來執行任務。
        1:  private async static void CallWithAsync()   2:   3:  {   4:   5:  try   6:   7:  {   8:   9:  Task<string> t1 = GreetingAsync("Bulbul");   10:   11:  Task<string> t2 = GreetingAsync("Ahmed");   12:   13:  await Task.WhenAll(t1, t2);   14:   15:  }   16:   17:  catch (Exception ex)   18:   19:  {   20:   21:  Console.WriteLine(&ldquo;handled {0}&rdquo;, ex.Message);   22:   23:  }   24:   25:  }
        捕獲所有任務的錯誤一種方法是在try塊之外聲明任務,這樣可以從try塊進行訪問,並檢查任務的“IsFaulted”屬性。如果它存在異常那麼“IsFaulted”屬性值為True,就可捕獲任務實例的內部異常。
        還有另一個更好的辦法:   1:  static async void ShowAggregatedException()   2:   3:  {   4:   5:  Task taskResult = null;   6:   7:  try   8:   9:  {   10:  Task<string> t1 = GreetingAsync("Bulbul");   11:   12:  Task<string> t2 = GreetingAsync("Ahmed");   13:   14:  await (taskResult = Task.WhenAll(t1, t2));   15:   16:  }   17:   18:  catch (Exception ex)   19:   20:  {   21:   22:  Console.WriteLine("handled {0}", ex.Message);   23:   24:  foreach (var innerEx in taskResult.Exception.InnerExceptions)   25:   26:  {   27:  Console.WriteLine("inner exception {0}", nnerEx.Message); }   28:  }   29:   30:  }
        取消任務
        在此之前,如果從線程池中調用線程,線程是不可能取消。現在,Task類提供了一個方法基於CancellationTokenSource類能夠取消已啟動的任務,取消任務步驟:
        1. 異步方法應該除外 “ CancellationToken” 參數類型   2. 創建CancellationTokenSource類實例:   var cts =new CancellationTokenSource();   3. 傳遞CancellationToken,如:   1:    Task<string> t1 = GreetingAsync("Bulbul", cts.Token);   4. 長時間運行的方法中,必須調用CancellationToken 的ThrowIfCancellationRequested()方法。   1:   static string Greeting(string name, CancellationToken token){   2:   3:  Thread.Sleep(3000);   4:   5:  token. ThrowIfCancellationRequested();   6:   7:  return string.Format("Hello, {0}", name);   8:   9:  }   5. 從等待的Task中捕獲 OperationCanceledException異常。   6. 如果通過調用CancellationTokenSource的實例的方法執行取消操作,將從長時間運行操作中拋出OperationCanceledException異常。也可以設置取消的時間。以下是完整的代碼,一秒後執行取消操作:   1:   static void Main(string[] args)   2:   3:   {   4:   CallWithAsync();   5:   6:  Console.ReadKey();   7:   8:   }   9:   10:   11:  async static void CallWithAsync()   12:   13:  {   14:   15:  try   16:   17:   {   18:   19:  CancellationTokenSource source = new CancellationTokenSource();   20:   21:  source.CancelAfter(TimeSpan.FromSeconds(1));   22:   23:   var t1 = await GreetingAsync("Bulbul", source.Token);   24:   }   25:   26:   catch (OperationCanceledException ex)   27:   28:  {   29:   30:   Console.WriteLine(ex.Message);   31:   32:   }   33:   34:   }   35:   36:  static Task<string> GreetingAsync(string name, CancellationToken token)   37:   38:   {   39:   40:   return Task.Run<string>(() =>   41:   42:   {   43:   44:   return Greeting(name, token);   45:   46:   });   47:  }   48:   49:   50:   static string Greeting(string name, CancellationToken token)   51:   52:   {   53:   54:  Thread.Sleep(3000);   55:   token.ThrowIfCancellationRequested();   56:   57:   return string.Format("Hello, {0}", name);   58:   59:   }   60:
        並行編程
        .NET 4.5及以上版本推出“Parallel類,是線程類的抽象。使用“Parallel”類,我們可以實現並行。並行與線程不同,它使用所有可用的CPU或內核的。以下兩種類型的並行是可行:
        數據並行:如果我們有數據的大集合,我們希望在每個數據的某些操作進行並行使用,那麼就可以使用數據並行。Parallel類有靜態For或ForEach來執行數據並行行,如
        1:  ParallelLoopResult result =   2:                      Parallel.For(0, 100, async (int i) =>   3:                      {   4:                          Console.WriteLine("{0}, task: {1}, thread: {2}", i,   5:                          Task.CurrentId, Thread.CurrentThread.ManagedThreadId);   6:                          await Task.Delay(10);   7:   8:                });
        For或ForEach方法可以在多線程中和且索引無序可以是無序的。   如果想停止並行For或ForEach方法,可通過ParallelLoopState作為參數,並根據需要打破循環的狀態,跳出循環。
        1:  ParallelLoopResult result =   2:                      Parallel.For(0, 100, async (int i, ParallelLoopState pls) =>   3:                      {   4:                          Console.WriteLine("{0}, task: {1}, thread: {2}", i,   5:                          Task.CurrentId, Thread.CurrentThread.ManagedThreadId);   6:                          await Task.Delay(10);   7:                          if (i > 5) pls.Break();   8:                });   2. 任務並行:如果想要同時運行多個任務的,我們可以通過調用Parallel類的invoke方法使用任務並行Parallel.Invoke方法接收委托行為的數組。例如:   1:  static void ParallelInvoke()   2:   3:  {   4:   5:  Parallel.Invoke(MethodOne, MethodTwo);   6:   7:  }   8:
        結論   本文詳細介紹了.NET Framework 4.5提供的異步編程技術及細節。
    1. 上一頁:
    2. 下一頁:
    Copyright © 程式師世界 All Rights Reserved