基于任务的异步编程

我是研究僧i 提交于 2020-02-29 04:41:08

博客迁移

记录《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> 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);
    }
}
View Code
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);
    }
}
View Code

如果希望有些异常能从中恢复

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) //如果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/OWeb请求,文件异步可以通过端口实现,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更新

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!