记录《Effective C#》学习过程。
任务运行的几种方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Task task = new Task(() => { Console.WriteLine($"task1的线程ID为{Thread.CurrentThread.ManagedThreadId} " ); }); task.Start(); Task task2 = Task.Factory.StartNew(() => { Console.WriteLine($"task2的线程ID为{Thread.CurrentThread.ManagedThreadId} " ); }); Task task3 = Task.Run(() => { Console.WriteLine($"task3的线程ID为{ Thread.CurrentThread.ManagedThreadId} " ); });
使用异步方法执行异步工作
对于调用异步方法的主调方法来说,只要异步方法已经返回,这里返回的是Task对象,它就可以继续往下执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public static async void MainMethod ( ) { var task = TaskMethod(); TaskStatus taskStatus = task.Status; 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; }
主调用方法执行到await的时候Task如果已经完成,就会返回一个已完成状态的Task对象,并且继续执行await的下一条语句,就像同步一样。
主调用方法执行到await的时候Task如果已经还未完成,底层的机制就是编译器把await后面的语句生成delegate,写入相应的状态信息。直到任务完成,会有一个SynchronizationContext类恢复delegate运行的情境到await之前的样子(控制台是没有SynchronizationContext的)。
一定要等候任务的执行结果,否则有异常也不会抛出来。
await后面的语句,可能是当前线程来做,也可能是另一条线程。
Task.Wait()、Task.Result可以做到等候Task执行完毕,才往下跑,但是会让当前线程阻塞。
不要写返回值类型为void的异步方法
主调方法调用返回返回值为void的异步方法,如果异步方法执行报错,主调方法无法catch到它的异常。只能通过App.Domain.UnhandleException事件或其他非常规手段来处理异常。
通过AppDomain.UnhandleExceptioin事件处理异常并不能让程序从异常中恢复。
无法等待返回值为void的异步方法的执行结果,就无法轻易判断它什么时候执行完。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private async void Button1_Click (object sender, EventArgs e ) { try { Test(); } catch (Exception ex) { } } static async void Test ( ) { var task = GetTask(); var result = await task; } [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(); }
如果要写返回值为void的异步方法,一定要做好异常处理
第一种:简单的记录异常,不会妨碍程序继续往下执行
1 2 3 4 5 6 7 8 9 10 11 12 static async void Test1 ( ) { try { var task = GetTask(); await task; } catch (Exception ex) { Log(ex.ToString()); } }
第二种:借助异常过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static async void Test1 ( ) { try { var task = GetTask(); await task; } catch (Exception ex)when (LogMessage(ex)) { } } static bool LogMessage (Exception ex ) { Log(ex.ToString()); return false ; }
第三种:把所执行的异步工作视为Task,处理异常的逻辑分别表示通用的Action<Exception>
、Func<Exception,bool>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static async void Test1 (this Task task,Action<Exception> onErrors ) { try { await task; } catch (Exception ex) { onErrors(ex); } } static async void Test2 (this Task task, Func<Exception,bool > onErrors ) { try { await task; } catch (Exception ex)when (onErrors(ex)) { onErrors(ex); } }
假如希望有些异常能从中恢复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static async void Test2<TException>(this Task task, Action<TException> recovery,Func<Exception,bool > onError) where TException : Exception { try { await task; } catch (Exception ex)when (onError(ex)) { } catch (TException ex2) { recovery(ex2); } }
不要同步方法与异步方法组合使用 第一种情况:同步方法里调用异步方法
原因一:捕获异常麻烦,通过Task.Wait()
或者Task.Result
来等待Task执行完毕,系统所抛出的异常是非具体的,而是AggregateException
类型异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ; } }
原因二:代码如下,可能发生死锁,GUI及Asp.Net情境下的SynchronizationContext只包含一条线程。Task.Wait()
会让这条线程阻塞,而await下面的语句又需要这条线程才能跑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ; }
与Thread.Sleep相比,Task.Delay是一种异步的延时机制,允许线程去做其他事。
第二种情况:异步里启动另一个异步任务,并在另一个异步任务里执行计算量较大的同步操作。
原因一:本来就有线程执行这项异步操作,没必要需要开辟更多的线程执行。
原因二:异步方法开辟新的线程执行计算量较大的同步操作,误导开发调用者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private async void Button1_Click (object sender, EventArgs e ) { MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString()); await GetTaskAsync(); } public double ComputeValue ( ) { MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString()); 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()); Task.Run(() => ComputeValue()); return 2 ; }); task.Start(); var result = await task; return result; }
异步任务嵌套异步任务是可以的,只是应该是将自己无法完成或者不便完成的任务交给另外的异步去做,而不是随意开辟新的线程,把本来就可以自己执行的工作转交出去。
使用异步方法,要考虑线程分配和上下文切换的开销 可以异步,但不要随便用。
原因一:线程成本,当前线程就能做好的工作转交给另一个线程做、前面线程的确减轻负担,但后面线程也增加负担了。所以在当前线程是稀缺且重要的资源,例如GUI应用程序的UI线程,才应该把计算量较大的工作转交给其他异步去做。
原因二:上下文切换成本,await任务之后,可以正常往下执行,是因为SynchronizationContext
记住了await之前的所有状态。等任务执行完后,切换到原来的SynchronizationContext
。
有些异步没有必要开辟新线程,例如文件异步I/O 、Web请求 ,文件异步可以通过端口实现,Web请求可以通过网络中断实现。
如果await语句之后的代码与上下文无关,可以通过调用Task对象的ConfigureAwait(false)
告诉系统不必切回到原理捕获的上下文中运行,默认是true。
使用ConfigureAwait(false)
好处是提高性能,避免死锁。
1 2 3 4 5 6 7 8 9 private async void Button1_Click (object sender, EventArgs e ) { await GetTaskAsync().ConfigureAwait(continueOnCapturedContext:false ); }
如果是在某条await语句处调用ConfigureAwait(false),而且这里await的任务是异步执行的,系统会把下面的代码安排到默认的上下文中去,一旦这样做,很难切回最初捕获的上下文。
1 2 3 4 5 6 7 8 9 private async void Button1_Click (object sender, EventArgs e ) { await GetTaskAsync().ConfigureAwait(continueOnCapturedContext:false ); await GetTaskAsync(); await GetTaskAsync(); string aa = "" ; }
但是可以通过调整代码结构,把与上下文无关的代码移到新的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private async void OnCommand (object sender,RoutedEventArgs e ) { var viewModel = DataContext as SampleViewModel; try { Config config = await ReadConfigAsync(viewModel); await viewModel.Update(config); } 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 ); config = await ParseConfig(result) .ConfigureAwait(false ); } else { config = new Config(); } return config; }
如果编写的是应用程序级代码,不要使用ConfigureAwait(false)
,避免程序崩溃。详细阅读ConfigureAwait常见问题解答
Task对象 Task对象只是执行异步的一个载体,它有几个重要的方法。Task.WhenAll Task.WhenAny
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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()); var results = await Task.WhenAll(tasks); var result = await (await Task.WhenAny(tasks)); } private async Task<int > GetTask ( ) { var task = new Task<int >(() => { return 5 ; }); task.Start(); return await task; }
如果有多项任务,而且要求必须对已经执行的每项任务的结果做一些处理,这些任务不会互相依赖。在考虑性能的情况下,当然想哪些先完成,哪些结果就先拿来处理,首先想到是用WhenAny方法,但是每一次WhenAny就创建一项新任务,效率不太好。这时可以考虑使用TaskCompletionSource,这是一个可以容纳异步任务执行结果的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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 => { 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; } public Task ContinueWith (Action<Task<TResult>> continuationAction, CancellationToken cancellationToken, TaskContinuationOptions continuationOptions, TaskScheduler scheduler ) ; [Flags ] public enum TaskContinuationOptions { ...... ...... ExecuteSynchronously = 524288 }
考虑任务支持取消功能 可以通过CancellationToke这个struct类型实现任务的取消功能,如果调用者请求取消,则ThrowIfCancellationRequested()方法会抛出System.OperationCanceledException异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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(); } public interface IProgress<T, T1> { void Report (T t, T1 t1 ) ; }
调用方可以通过CancellationTokenSource对象请求取消
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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) { } }
如果异步任务方法的返回值是void,调用方无法遵循正常途径处理异常,只能通过专门的处理程序处理异常。因此,建议返回值为void的异步方法不支持取消功能。
缓存异步方法的返回值 如果程序因为频繁分配Task对象而使得效率低下,可以考虑使用ValueTask优化。ValueTask提供了一个接受Task参数的构造函数,ValueTask是Struct类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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换成ValueTask,如果需要实时获取数据就没必要使用ValueTask。
参考书籍:《Effective C#》进阶篇,针对C# 7.0更新
Last updated: 2020-02-14 17:03:38