.NET Core多线程通关 异步 - 续
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.NET Core多线程通关 异步 - 续相关的知识,希望对你有一定的参考价值。
【.NET Core】| 总结/Edison Zhou
大家好,我是Edison。
去年换工作时系统复习了一下.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》
年终总结:Edison的2022年终总结
数字化转型:我在传统企业做数字化转型
C#刷题:C#刷剑指Offer算法题系列文章目录
.NET面试:.NET开发面试知识体系
.NET大会:2020年中国.NET开发者大会PDF资料
以上是关于.NET Core多线程通关 异步 - 续的主要内容,如果未能解决你的问题,请参考以下文章