• 【译】异步编程-技巧和窍门


    C#异步/等待模式的出现引入了编写良好且可靠的并行代码的新方法,但是,随着创新不断发生,它也引入了将许多的的新方法。很多时候,当尝试使用async / await解决多线程问题时,程序员不仅不解决旧问题,还创建新的问题,当死锁,饥饿和竞争条件仍然存在时,甚至更难找到它们。

    所以我只是想在这里分享我的一些经验。也许它将使某人的生活更轻松。但是首先,让我们先学习一个简短的历史课程,看看异步/等待模式是如何在我们的生活中出现的,以及借助它可以解决哪些问题。这一切都从回调开始,当我们只有一个函数时,作为第二个动作,我们传递动作,此后称为。就像是:

    void Foo(Type parameter, Action callback) {...}
    void Bar() {
        some code here
        ...
        Foo(parameter, () => {...});
    }
    

    这很酷,但是这些回调结构有很大的增长趋势。

    然后是Microsoft APM(异步编程模型),它在回调,异常方面也或多或少存在类似问题,并且仍不清楚在哪个线程代码中执行。下一步是实现基于EAP事件的异步模式。EAP引入了已知的异步命名约定,其中最简单的类可能只有一个MethodName Async方法和一个相应的MethodName Completed事件,但仍然感觉很像回调。这很有趣,因为它表明现在名称中具有异步的所有内容都将返回任务。

    public class AsyncExample  
    {  
        // Synchronous methods.  
        public int Method1(string param);  
        public void Method2(double param);  
      
        // Asynchronous methods.  
        public void Method1Async(string param);  
        public void Method1Async(string param, object userState);  
        public event Method1CompletedEventHandler Method1Completed;  
       
        public bool IsBusy { get; }   
    }
    

    最后,我们来谈谈我们都喜欢并知道的TAP或任务异步模式,对吗?TAP中的异步方法在Async操作名称之后包含后缀,该后缀用于返回等待类型的方法,例如TaskTask.ResultValueTaskValueTaskResult。通过这种方法,引入了一个Task对象,与上面的模式相比,它给我们带来了很多好处。

    • 代码执行状态,例如 Cancelled, Faulted, RanToCompletion
    • 明确的任务取消 CancellationToken
    • TaskScheduler 这有助于代码执行上下文

    现在,通过简短的历史介绍,让我们跳到一些更实际的事情,这些事情可以使我们的生活更轻松一些。我将尝试并提及一些我最常用的最重要的做法。

    .ConfigureAwait(假)
    我经常从同事那里听到消息,也经常在帖子中读到,您遇到了死锁问题,可以.ConfigureAwait(false)在任何地方使用它,您会没事的,我真的不同意。尽管使用.ConfigureAwait(false)可能确实有益,但牢记某些事情始终很重要。例如,ConfigureAwait在需要上下文的方法中,在等待之后有代码时,不应使用。CLR控制将在await关键字之后执行哪个线程代码,而.ConfigureAwait(false)我们基本上是说我们不在乎在await关键字之后执行哪个线程代码。这意味着,如果我们使用UI或在ASP.Net中进行操作,则可以使用HttpContext.Current或构建http响应,我们始终需要在主线程中继续执行。但是,如果您正在编写一个库,但不确定如何使用该库,则使用它是一个好主意.ConfigureAwait(false)-通过ConfigureAwait这种方式使用,可以实现少量的并行处理:某些异步代码可以并行运行主线程,而不是不断地给它添加一些工作要做。

    CancellationToken
    CancellationToken通常,将类与任务一起使用是一个好主意,此机制是控制任务执行流程的简便实用工具,特别适用于可能被用户停止的长执行方法。这可能是繁重的计算过程,长时间运行的数据库请求或仅仅是网络请求。

    public async Task MakeACallClickAsync(CancellationToken cancellationToken)
    {
      try
      {
        var result = await GetResultAsync(cancellationToken);
      }
      catch (OperationCanceledException) // includes TaskCanceledException
      {
        MessageBox.Show("Your operation was canceled.");
      }
    }
    
    
    public async Task<MyResult> GetResultAsync(CancellationToken cancellationToken)
    {
      try
      {
        return await httpClient.SendAsync(httpRequestMessage, cancellationToken);
      }
      catch (OperationCanceledException)
      {
        // perform your cleanup if necessary    
      }
    }
    

    请注意,有两种取消异常类型:TaskCanceledExceptionOperationCanceledExceptionTaskCanceledException派生自OperationCanceledException。因此,在编写用于处理已取消操作的失败的catch块时,最好捕获OperationCanceledException,否则某些取消事件会在您的catch块中漏出并导致无法预测的结果。

    .Result / Wait()
    使用这种方法非常简单-尽量避免使用它,除非您对自己的工作有100%的把握,但仍然如此await。微软表示,这Wait(TimeSpan)是一种同步方法,可使调用线程等待当前任务实例完成。
    还记得我们提到过,任务总是在上下文中执行的,而clr控件将在其中执行线程连续吗?看下面的代码:

    // Service method.
    public async Task<JsonResult> GetJsonAsync(Uri uri)
    {
      
      var client = _httpClientFactory.CreateClient();  
      var stream = await _httpClient.GetStreamAsync("https://really-huge.json");
      using var sr = new StreamReader(stream);
    	using var reader = new JsonTextReader(sr);
      while (reader.Read())
      {
    	.... get json result
      }
      return jsonResult;  
    }
    
    // Controller method.
    public class MyController : Controller
    {
    
      [HttpGet]
      public string Get()
      {
        var jsonTask = GetJsonAsync(Uri.Parse("https://somesource.com"));
        return jsonTask.Result.ToString();
      }
    }
    
    • 在控制器中,我们称为GetJsonAsync(ASP.NET上下文)。
    • http请求_httpClient.GetStreamAsync("https//really-huge.json")已启动
    • 然后GetStreamAsync返回未完成的任务,指示请求未完成。
    • GetJsonAsync等待GetStreamAsync返回的任务。上下文已保存,将用于继续运行GetJsonAsync方法。GetJsonAsync返回未完成的Task,指示GetJsonAsync方法尚未完成。
    • 通过jsonTask.Result.ToString();在控制器中使用,我们可以同步阻止GetJsonAsync返回的Task。这将阻塞主上下文线程。
    • 在某个时刻,GetStreamAsync将完成并且其Task将完成,在此之后,GetJsonAsync准备好继续,但是它等待上下文可用,以便可以在上下文中执行。然后我们有一个死锁,因为控制器方法在等待*GetJsonAsync完成的同时阻塞了上下文线程,而GetJsonAsync在等待上下文可用以便可以完成。

    这种死锁不容易发现,并且经常会引起很多不适,这就是为什么不建议使用Wait()和.Result的原因。

    Task.Yield()
    老实说,这是一个小技巧,我没用太多,但是拥有您的武器库是一件好事。这个问题是,当使用async/await,并不能保证您await FooAsync()实际使用时会异步运行。内部实现可以使用完全同步的路径自由返回。假设我们有一些方法:

    async Task MyMethodAsync() {
        someSynchronousCode();
        await AnotherMethodAsync();
        continuationCode();
    }
    

    看起来我们没有在这里阻塞任何东西,并且希望await AnotherMethodAsync();也可以异步运行,但是让我们看一下幕后会发生什么。当我们的代码被编译时,我们可以得到类似于下面的代码(非常简化):

    async Task MyMethodAsync() {
      
      someSynchronousCode();
      
      var awaiter = AnotherMethodAsync().GetAwaiter();
      
      if (!awaiter.isCompleted)
      {
        compilerLogic(continuationCode()); // asynchronous code
      }
      else
      {
        continuationCode(); // synchronous code
      }
    }
    

    这是发生了什么:

    • omeSynchronousCode()将按预期同步运行。
    • 然后AnotherMethodAsync() 是同步执行,那么我们得到与.GetAwaiter一个awaiter对象()
    • 通过检查waiter.IsCompleted,我们可以查看任务是否完成
    • 如果任务完成,那么我们只需同步运行continuationCode()
    • 如果任务未完成,则continuationCode()计划在任务上下文中执行

    结果,即使await仍然可以同步执行代码,并且通过使用代码,await Task.Yield()我们始终可以保证!awaiter.IsCompleted并强制该方法异步完成。有时它可以有所作为,例如在UI线程中,我们可以确保我们长时间不忙。

    ContinueWith()

    常见的情况是,一个操作完成后,我们需要调用第二个操作并将数据传递给它。传统上,回调方法用于运行延续。在任务并行库中,延续任务提供了相同的功能。Task类型公开的多个重载ContinueWith。此方法创建一个新任务,该任务将在另一个任务完成时进行安排。让我们看一下一个非常常见的情况,我们正在下载图像并对每个图像进行一些处理。我们需要按顺序进行处理,但是我们希望尽可能多的并发下载,而且还要ThreadPool对下载的图像执行计算密集型处理:

    List<Task<Bitmap>> imageTasks = urls.Select(u => 
              GetBitmapAsync(imageUrl)
              .ContinueWith((t) => {
                  if (t.Status == TaskStatus.RanToCompletion) {
                        ConvertImage(t.Result)
                  }
                  else if (t.Status == TaskStatus.Faulted) {
                        _logger.Log(t.Exception.GetBaseException().Message);
                  }
              })).ToList();
    
    while(imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);
    
            Bitmap image = await imageTask;
            .... add orr store the image .....
            .... .AddImage(image);
        }
        catch{}
    }
    

    在这种情况下,使用起来非常方便,ContinueWith()因为与之后的代码不同awaitContinueWith()逻辑将在线程池上执行,这是我们进行计算并防止主线程阻塞所需要的。
    请注意,Task.ContinueWith 将对上一个对象的引用传递给用户委托。如果先前的对象是System.Threading.Tasks.Task对象,并且任务运行完毕,则可以执行任务的Task.Result属性。实际上,.Result属性会阻塞,直到任务完成为止,但是ContinueWith()当任务状态更改时,也会由另一个Task调用。这就是为什么我们首先检查状态,然后才进行处理或记录错误的原因。

    原文相关

     原文作者:Eduard Los
     原文地址:https://medium.com/@eddyf1xxxer/bi-directional-streaming-and-introduction-to-grpc-on-asp-net-core-3-0-part-2-d9127a58dcdb
  • 相关阅读:
    IDEA04 工具窗口管理、各种跳转、高效定位、行操作、列操作、live template、postfix、alt enter、重构、git使用
    Maven01 环境准备、maven项目结构、编译/测试/打包/清除、安装、
    SpringBoot31 整合SpringJDBC、整合MyBatis、利用AOP实现多数据源
    Docker03 Docker基础知识、Docker实战
    [leetcode数组系列]2三数之和
    [leetcode数组系列]1 两数之和
    时间复杂度总结
    《将博客搬至CSDN》
    5 系统的软中断CPU升高,一般处理办法?
    python数据分析5 数据转换
  • 原文地址:https://www.cnblogs.com/ancold/p/12955182.html
Copyright © 2020-2023  润新知