博客遷移
記錄《Effective C#》學習程序,
任務運行的幾種方法
//1.new方式實體化一個Task,需要通過Start方法啟動 Task task = new Task(() => { Console.WriteLine($"task1的執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)創建和啟動一個Task Task task2 = Task.Factory.StartNew(() => { Console.WriteLine($"task2的執行緒ID為{Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)將任務放在執行緒池佇列,回傳并啟動一個Task Task task3 = Task.Run(() => { Console.WriteLine($"task3的執行緒ID為{ Thread.CurrentThread.ManagedThreadId}"); });View Code
使用異步方法執行異步作業
對于呼叫異步方法的主調方法來說,只要異步方法已經回傳,這里回傳的是Task物件,它就可以繼續往下執行,
public static async void MainMethod() { var task = TaskMethod(); //呼叫的開始,異步方法就在跑了 TaskStatus taskStatus = task.Status; //任務的狀態 //Created = 0, //WaitingForActivation = 1, //WaitingToRun = 2, //Running = 3, //WaitingForChildrenToComplete = 4, //RanToCompletion = 5, //Canceled = 6, //Faulted = 7 var a = ""; var b = ""; var c = ""; var d = ""; var result = await task; var sum = result + 2000; } public static async Task<int> TaskMethod() { var task = GetTask(); return await task; }View Code
主呼叫方法執行到await的時候,Task如果已經完成,則會回傳一個已完成狀態的Task物件,并且繼續執行await的下一條陳述句,就像同步一樣,
主呼叫方法執行到await的時候,Task如果還未完成,編譯器把await后面的陳述句生成delegate,寫入相應的狀態資訊,直到任務完成,會有一個SynchronizationContext類恢復delegate運行的情境到await之前的樣子(控制臺是沒有SynchronizationContext的),
一定要等候任務的執行結果,否則有例外也不會拋出來,
Task.Wait()、Task.Result等候Task執行完畢,才往下跑,但是會讓當前執行緒阻塞,
不要寫回傳值型別為void的異步方法
主調方法呼叫回傳回傳值為void的異步方法,如果異步方法執行報錯,主調方法無法catch到它的例外,只能通過App.Domain.UnhandleException事件或其他非常規手段來處理例外,
通過AppDomain.UnhandleExceptioin事件處理例外并不能讓程式從例外中恢復,
無法等待回傳值為void的異步方法的執行結果,就無法輕易判斷它什么時候執行完,
private async void Button1_Click(object sender, EventArgs e) { try { Test(); } catch(Exception ex) { //斷點進不到catch } } //回傳值為void的異步方法 static async void Test() { var task = GetTask(); var result = await task; } /// <summary> /// 應用程式的主入口點, /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Application.Run(new Form1()); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { //斷點可以進來 throw new NotImplementedException(); }View Code
如果要寫回傳值為void的異步方法,一定要做好例外處理
第一種:簡單的記錄例外,不會妨礙程式繼續往下執行
static async void Test1() { try { var task = GetTask(); await task; } catch (Exception ex) { Log(ex.ToString()); //偽代碼 } }View Code
第二種:借助例外過濾器
static async void Test1() { try { var task = GetTask(); await task; } catch(Exception ex)when(LogMessage(ex)) { //1:如果LogMessage回傳true,可以catch到例外,程式還能往下執行, //如果catch里面又拋出例外,另說, //2:第二如果LogMessage回傳false,catch不到例外,會把例外重新拋出, //能在AppDomain.CurrentDomain.UnhandledException捕捉,整個程式會 //停掉 } } static bool LogMessage(Exception ex) { Log(ex.ToString()); //偽代碼 return false; }View Code
第三種:把所執行的異步作業視為Task,處理例外的邏輯分別表示通用的Action<Exception>、Func<Exception,bool>
static async void Test1(this Task task,Action<Exception> one rrors) { try { await task; } catch(Exception ex) { one rrors(ex); } } static async void Test2(this Task task, Func<Exception,bool> one rrors) { try { await task; } catch (Exception ex)when(onErrors(ex)) { one rrors(ex); } }View Code
static async void Test1(this Task task,Action<Exception> one rrors) { try { await task; } catch(Exception ex) { one rrors(ex); } } static async void Test2(this Task task, Func<Exception,bool> one rrors) { try { await task; } catch (Exception ex)when(onErrors(ex)) { one rrors(ex); } }View Code
如果希望有些例外能從中恢復
static async void Test2<TException>(this Task task, Action<TException> recovery,Func<Exception,bool> one rror) where TException : Exception { try { await task; } catch (Exception ex)when(onError(ex)) { } catch(TException ex2) //如果onError回傳false,就有可能catch到TException,并從中恢復 { recovery(ex2); } }View Code
不要同步方法與異步方法組合使用
原因一:同步調異步,無非就是Task.Wait()或者Task.Result實作,但這兩個方法拋出的例外是非具體的,而是AggregateException型別例外,真正的例外在這個例外里面,
public static int GetSum() { try { var task1 = GetTask1(); var task2 = GetTask2(); var result1 = task1.Result; var result2 = task2.Result; return result1 + result2; } catch(AggregateException e)when(e.InnerExceptions.FirstOrDefault().GetType()==typeof(KeyNotFoundException)) { return 0; } }View Code
原因二:代碼如下,可能發生死鎖,
舉例:
GUI及Asp.Net情境下的SynchronizationContext只包含一條執行緒,
Task.Wait()會讓執行緒阻塞,而await下面的陳述句又需要這條執行緒才能跑,
private async void Button1_Click(object sender, EventArgs e) { var task = Test(); string a = ""; string b = ""; string c = ""; string d = ""; _ = task.Result; Console.WriteLine("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaa"); } static async Task<bool> Test() { await Task.Delay(2000); string a = ""; string b = ""; string c = ""; string d = ""; return true; }View Code
上面例子補充:與Thread.Sleep相比,Task.Delay是一種異步的延時機制,允許執行緒去做其他事,
第二種情況:異步里啟動另一個異步任務,并在另一個異步任務里執行計算量較大的同步操作,
原因一:本來就有執行緒執行這項異步操作,沒必要需要開辟更多的執行緒執行,
原因二:異步方法開辟新的執行緒執行計算量較大的同步操作,誤導開發呼叫者,
private async void Button1_Click(object sender, EventArgs e) { MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString()); //除錯得到 當前執行緒ID:1 await GetTaskAsync(); } public double ComputeValue() { MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString()); //除錯得到 當前執行緒ID:4 double finalAnswer = 0; for (int i = 0; i < 100000000; i++) { finalAnswer += i; } return finalAnswer; } public async Task<double> GetTaskAsync() { var task = new Task<double>(()=> { MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString()); //除錯得到 當前執行緒ID:3 Task.Run(() => ComputeValue()); return 2; }); task.Start(); var result = await task; return result; }View Code
異步任務嵌套異步任務是可以的,只是應該是將自己無法完成或者不便完成的任務交給另外的異步去做,而不是隨意開辟新的執行緒,把本來就可以自己執行的作業轉交出去,
使用異步方法,要考慮執行緒分配和背景關系切換的開銷
可以異步,但不要隨便用,
原因一:執行緒成本,當前執行緒就能做好的作業轉交給另一個執行緒做、前面執行緒的確減輕負擔,但后面執行緒也增加負擔了,所以在當前執行緒是稀缺且重要的資源,例如GUI應用程式的UI執行緒,才應該把計算量較大的作業轉交給其他異步去做,
原因二:背景關系切換成本,await任務之后,可以正常往下執行,是因為SynchronizationContext記住了await之前的所有狀態,等任務執行完后,切換到原來的SynchronizationContext,
有些異步沒有必要開辟新執行緒,例如檔案異步I/O、Web請求,檔案異步可以通過埠實作,Web請求可以通過網路中斷實作,
ConfigureAwait(false)方法使用
如果await陳述句之后的代碼與背景關系無關,可以通過呼叫Task物件的ConfigureAwait(false)告訴系統不必切回到原理捕獲的背景關系中運行,默認是true,
使用ConfigureAwait(false)好處是提高性能,避免死鎖,
private async void Button1_Click(object sender, EventArgs e) { await GetTaskAsync().ConfigureAwait(continueOnCapturedContext:false); //一般不在應用程式級別代碼使用false,這里只是舉例子, //必須在特定的背景關系中執行,如果上面設為false //拋例外 System.InvalidOperationException:“執行緒間操作無效: //從不是創建控制元件“button2”的執行緒訪問它,” //button2.Text = "dddd"; }
如果是在某條await陳述句處呼叫ConfigureAwait(false),而且這里await的任務是異步執行的,系統會把下面的代碼安排到默認的背景關系中去,一旦這樣做,很難切回最初捕獲的背景關系,
private async void Button1_Click(object sender, EventArgs e) { await GetTaskAsync().ConfigureAwait(continueOnCapturedContext:false); await GetTaskAsync(); await GetTaskAsync(); string aa = ""; //在默認的背景關系中執行,回不到第一個await之前捕獲的背景關系了, }
但是可以通過調整代碼結構,把背景關系無關的代碼移到新方法,
private async void OnCommand(object sender,RoutedEventArgs e){ var viewModel = DataContext as SampleViewModel; try{ Config config = await ReadConfigAsync(viewModel); await viewModel.Update(config); //更新UI控制元件,需要在特定的背景關系里 } catch(Exception ex)when(logMessage(viewModel,ex)){ } } //不需要在特定的背景關系中執行 private async Task<Config> ReadConfigAsync(SampleViewModel viewModel){ var userInput = viewModel.webSite; var result = await DownloadAsync(userInput).ConfigureAwait(false); var items = XELement.Parse(result); var userConfig = from node in items.Descendants() where node.Name == "Config" select node.Value; var configUrl = userConfig.SingleOrDefault(); if(configUrl != null){ result = await DownloadAsync(configUrl).ConfigureAwait(false); //雖然前面有了ConfigureAwait(false),但依然要寫上 config = await ParseConfig(result) .ConfigureAwait(false); } else{ config = new Config(); } return config; }View Code
如果撰寫的是應用程式級代碼,不要使用ConfigureAwait(false),避免程式崩潰,詳細閱讀ConfigureAwait常見問題解答
Task物件
Task物件只是執行異步的一個載體,它有幾個重要的方法:Task.WhenAll、Task.WhenAny,
private async void Button1_Click(object sender, EventArgs e) { var tasks = new List<Task<int>>(); tasks.Add(GetTask()); tasks.Add(GetTask()); tasks.Add(GetTask()); tasks.Add(GetTask()); tasks.Add(GetTask()); //WhenAll 會根據現有的一批任務創建一個新任務 var results = await Task.WhenAll(tasks); //Task.whenAny回傳的是最先執行完畢的那項任務 var result = await (await Task.WhenAny(tasks)); } private async Task<int> GetTask() { var task = new Task<int>(() => { return 5; }); task.Start(); return await task; }View Code
如果有多項任務,而且要求必須對已經執行的每項任務的結果做一些處理,這些任務不會互相依賴,在考慮性能的情況下,當然想哪些先完成,哪些結果就先拿來處理,首先想到是用WhenAny方法,但是每一次WhenAny就創建一項新任務,效率不太好,這時可以考慮使用TaskCompletionSource,這是一個可以容納異步任務執行結果的地方,
public static Task<T>[] OrderByCompletion<T>(this IEnumerable<Task<T>> tasks) { var sourceTasks = tasks.ToList(); var completionSources = new TaskCompletionSource<T>[sourceTasks.Count]; var outputTasks = new Task<T>[completionSources.Length]; for(int i = 0; i < completionSources.Length; i++) { completionSources[i] = new TaskCompletionSource<T>(); outputTasks[i] = completionSources[i].Task; } int nextTaskIndex = -1; //每項任務執行完后,然后執行的方法, Action<Task<T>> continuation = completed => { //Interlocked.Increment確保執行緒安全 var bucket = completionSources[Interlocked.Increment(ref nextTaskIndex)]; bucket.TrySetResult(completed.Result); }; foreach(var inputTask in sourceTasks) { //借用了委托,當任務完成后,在委托方法里處理任務結果 inputTask.ContinueWith(continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } return outputTasks; } // 摘要: // 創建根據 continuationOptions 中指定的條件加以執行的延續任務, // // 引數: // continuationAction: // 根據在 continuationOptions 中指定的條件運行的操作, 在運行時,委托將作為一個自變數傳遞給完成的任務, // continuationOptions: // 用于設定計劃延續任務的時間以及延續任務的作業方式的選項, public Task ContinueWith(Action<Task<TResult>> continuationAction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler); [Flags] public enum TaskContinuationOptions { ...... ...... // // 摘要: // 指定應同步執行延續任務, 指定此選項后,延續任務在導致前面的任務轉換為其最終狀態的相同執行緒上運行, 如果在創建延續任務時已經完成前面的任務,則延續任務將在創建此延續任務的執行緒上運行, // 如果前面任務的 System.Threading.CancellationTokenSource 已在一個 finally(在 Visual Basic // 中為 Finally)塊中釋放,則使用此選項的延續任務將在該 finally 塊中運行, 只應同步執行運行時間非常短的延續任務, 由于任務以同步方式執行,因此無需呼叫諸如 // System.Threading.Tasks.Task.Wait 的方法來確保呼叫執行緒等待任務完成, ExecuteSynchronously = 524288 }View Code
考慮任務支持取消功能
可以通過CancellationToke這個struct型別實作任務的取消功能,如果呼叫者請求取消,則ThrowIfCancellationRequested()方法會拋出System.OperationCanceledException例外,
public Task RunPayroll() => RunPayroll(new CancellationToken(), null); public Task RunPayroll(CancellationToken cancellationToken) => RunPayroll(cancellationToken, null); public Task RunPayroll(IProgress<int, string> progress) => RunPayroll(new CancellationToken(), null); public async Task RunPayroll(CancellationToken cancellationToken,IProgress<int,string> progress) { progress?.Report(0, "第一步"); var result0 = await RunTask0(); cancellationToken.ThrowIfCancellationRequested(); progress?.Report(1, "第二步"); var result1 = await RunTask1(); cancellationToken.ThrowIfCancellationRequested(); progress?.Report(1, "第三步"); var result2 = await RunTask2(); cancellationToken.ThrowIfCancellationRequested(); progress?.Report(1, "第四步"); var result3 = await RunTask3(); cancellationToken.ThrowIfCancellationRequested(); } /// <summary> /// 監控進度 /// </summary> /// <typeparam name="T"></typeparam> /// <typeparam name="T1"></typeparam> public interface IProgress<T, T1> { void Report(T t, T1 t1); }View Code
呼叫方可以通過CancellationTokenSource物件請求取消
private async void Button1_Click(object sender, EventArgs e) { var cts = new CancellationTokenSource(); try { var task = RunPayroll(cts.Token); cts.Cancel(); //取消 await task; } catch(OperationCanceledException ex) { } }View Code
如果異步任務方法的回傳值是void,呼叫方無法遵循正常途徑處理例外,只能通過專門的處理程式處理例外,因此,建議回傳值為void的異步方法不支持取消功能,
快取異步方法的回傳值
如果程式因為頻繁分配Task物件而使得效率低下,可以考慮使用ValueTask優化,ValueTask提供了一個接受Task引數的建構式,ValueTask是Struct型別,
public ValueTask<IEnumerable<int>> GetData(int a,int b) { if (a < b) { return new ValueTask<IEnumerable<int>>(cacheData); //從快取中取 } else { async Task<IEnumerable< int >> load() //內嵌異步方法 { var result = await RunTask(); return result; } return new ValueTask<IEnumerable<int>>(load()); //接受Task引數的建構式 } }View Code
千萬確認性能瓶頸是因為記憶體分配的開銷導致,再考慮把Task換成ValueTask,如果需要實時獲取資料就沒必要使用ValueTask,
參考書籍:《Effective C#》進階篇,針對C# 7.0更新
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/67177.html
標籤:C#
上一篇:VS 2019中修改C#語言版本
