• .NET Core多线程 (3) 异步 - 下


    合集:.NET Core多线程温故知新

     

    去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的《.NET 5多线程编程实战》课程,我将复习的知识进行了总结形成本专题。

    本篇,我们来继续复习一下异步的相关知识点,预计阅读时间10分钟。

    深入分析使用Result方法死锁的原因

    (1)慎用Result

    • 场景1:带有同步上下文的编程模型中有可能会出现死锁

      • 例如:WindowsForm、WPF

    • 场景2:同步+异步的场景中也有可能出现死锁

      • Result => 同步等待,它其实违背了异步编程的理念(初心)

      • 同步+异步混用会异常复杂,产生的Bug不易发现

        • 比如:在WindowsForm下,同步调用异步方法(task.GetResult())时,async的callback进入了Queue,而主线程需要不断地读取Queue的内容来执行,就容易造成死锁。

        • 为什么会出现死锁?

          • 主线程 要结束阻塞,必须要等待 延续Task 执行完毕

          • 延续Task 要执行完毕,必须要 主线程 从Queue中调取执行

    (2).NET中的解决方案

    方法一:不使用同步上下文(比如WindowsFormSynchronizationContext)

    在自己的IO线程中完成,就没有所谓的Queue了。

    var content = await client
      .GetStringAsync("http://cnblogs.com")
      .ConfigureAwait(false);

    方法二:不阻塞主线程

    即我们熟知的 async + await。

    复制代码
    private async void button1_Click(object sender, EventArgs e)
    {
        var content = await GetContent();
    
        textBox1.Text = content;
    }
    复制代码

    方法三:使用线程池完成

    用线程池中的thread执行(比如:Task.Run),不用 main thread。

    复制代码
    private void button1_Click(object sender, EventArgs e)
    {
       var task = Task.Run(() =>
       {
           var content = GetContent().Result;
    
           return content;
       });
    
       textBox1.Text = task.Result;
    }
    复制代码

    (3)开源项目中的解决方案

    比如Dapper这个开源项目中,它使用的是 task.ConfigureAwait(false) 的方式来避免死锁的。

    复制代码
    private void button1_Click(object sender, EventArgs e)
    {
       SqlConnection connection = new SqlConnection("Server=LocalHost; Persist Security Info=False;Integrated Security=SSPI;Database= PostDB;");
    
       var length = connection.ExecuteScalarAsync<int>("select count(1) from Post").Result;
    
       textBox1.Text = length.ToString();
    }
    复制代码

    常见的异步化编程模型

    (1)异步延迟

    Thread.Sleep方法的弊端:线程会休眠等待,等于浪费了资源。

    Task.Delay方法的好处:避免了线程的等待,让线程被高效利用;其底层是Timer实现的(worker thread),通过Timer调度之后会切换线程。

    await Task.Delay(1000 * 3);

    (2)异步流

    同步中的yield:不需要定义中间集合,可以延迟执行;

    yield return urls;

    异步中的yield:

    复制代码
    foreach (var url in await urlsTask)
    {
       if (url.Contains("csdn") || url.Contains("cnblogs"))
          yield return url;
    }
    复制代码

    Dapper项目中的案例:

    while (await reader.ReadAsync())
    {
       yield return reader;
    }

    (3)异步并发限制

    方法1:借助异步锁实现:SemaphoreSlim.WaitAsync方法;

    方法2:借助Task.WhenAll实现;

    比如:限制最多两个task并行

    复制代码
    foreach (var url in urls)
    {
        tasks.Add(client.GetStringAsync(url));
    
        if (tasks.Count == 2)
        {
            var strlist = await Task.WhenAll(tasks);
    
            Console.WriteLine($"{DateTime.Now}, length1={strlist[0].Length}, length2={strlist[1].Length} tid={Environment.CurrentManagedThreadId}");
    
            tasks.Clear();
        }
    }
    复制代码

    异步和并行开发中的异常处理

    (1)并行中的异常

    问题1:Task的Wait和Result下的异常如何捕获?

    Wait 针对无返回值,可以帮助捕获到;ExceptionResult 针对有返回值,可以帮助捕获到Exception;

    问题2:为什么得到的是AggregateException异常?

    因为,所谓的并行,肯定有多个Task,进而可能会抛多个Exception。而AggregateException相当于做了一个聚合,将所有Exception的Message组合在一起。

    问题3:延续任务中的异常又该如何捕获?

    比如,在延续task中发现了前面task有异常,怎么处理?

    方式1:处理

    if(t.IsFaulted)
    {
        t.Exception.Handle(m => true);
    }

    方式2:不处理,往外抛

    if(t.IsFaulted)
    {
        t.Exception.Handle(m => false);
    }

    问题4:全局异常又该如何捕获?

    在异步编程中可能会出现异常逃逸现象,如何全局发现那些被我们忽视的异常Task?

    解法:借助Finalize线程,在回收托管资源时,调用析构函数。

    示例:使用TaskScheduler.UnobservedTaskException 进行全局注册:

    复制代码
    TaskScheduler.UnobservedTaskException += (sender, e) =>
    {
       Console.WriteLine(e.Exception.Message);
       Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
    };
    
    GC.Collect(); // 仅用来测试
    复制代码

    (2)异常中的异常

    异常1:无await下的逃逸

    因为,IO线程在抛异常时,控制流已经超出了try-catch块了。

    异常2:在async avoid且有await下的逃逸

    我们需要在async avoid方法中增加try-catch异常捕获机制。

    关于异步的相关补充

    关于async/await的大致流程图,一图胜千言:

    关于IO完成端口(IOCP)的大致流程图,一图胜千言:

    小结

    本篇,我们复习了异步相关的基础知识,但由于内容太多,因此将其拆分为了两篇推文。

    下一篇,我们将复习一下锁机制的相关知识。

    参考资料

    一线码农,腾讯课堂《.NET 5多线程编程实战

    不明作者,《Task调度与await》

     

  • 相关阅读:
    【C++】笔试训练(五)
    Dcoker入门,小白也学得懂!
    《javascript忍者秘籍》笔记
    matlab习题 —— 矩阵的常规运算
    缓存使用常见思路及问题
    【SwiftUI项目】0010、SwiftUI项目-费用跟踪-记账App项目-第2/3部分 -过滤日期详情明细页面以及抽离费用卡片
    什么是数据管理能力成熟度评估(DCMM)
    模型微调(fine-tuning)
    spring boot中的标注@Component、@Service等
    【LeetCode】No.78. Subsets -- Java Version
  • 原文地址:https://www.cnblogs.com/edisonchou/p/dotnet_multithread_learning_notes_chap3.html