

C#中async的死锁分析和解决方案
source link: https://www.imzjy.com/blog/dotnet-async-locks-and-solutions?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C#中async的死锁分析和解决方案
如果你开发一个简单的Windows Form程序,点击Button去使用async
异步获取一个数据,然后显示在Label上,类似这样的代码
private void button1_Click(object sender, EventArgs e)
{
var task = GetContentAsync();
var content = task.Result;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var http = new HttpClient();
var result = await http.GetStringAsync("http://www.imzjy.com");
var first50 = result.Substring(0, 50);
return first50;
}
当你点击Button的时候会发现程序直接卡死了。
死锁原因分析
C#中的async/await
隐藏了很多的细节,一个简单的await
其实让函数发生了一次重入,重入对于多线程代码来说其实很正常。但是C#将这些藏了起来。你看上去像一个函数,其实被分成了两段,而且执行这两段代码的线程还可能不一样。
上面代码真正的执行过程是这样的:
private void button1_Click(object sender, EventArgs e)
{
//1. calling GetContentAsync
var task = GetContentAsync();
Debug.WriteLine($"Continuation:{Environment.CurrentManagedThreadId}");
//4. .Result(or GetAwait().GetResult()) which waiting for GetContentAsync to complete.
//OOPS: DEADLOCK!!!
//REASON: task.Result waiting the http.GetStringAsync complete and return;
//REASON: GetContentAsync wait button1_Click release the synchronization context;
var content = task.Result;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var http = new HttpClient();
//2. automatic capture synchronization context :: auto capture caused the issue.
//3. due to await applied, yield thread to caller(button1_Click)
var result = await http.GetStringAsync("http://www.imzjy.com");
//WHY AUTO CAPTURE? capture the synchronization context makes following accessing UI control became possible.
//textBox1.Text = first50;
var first50 = result.Substring(0, 50);
return first50;
}
对于GetContentAsync
函数来说,在await
之前其实是同步的代码,当await
之后,线程直接返回给button1_Click
。await
时候发生了两件事:
- 在返回之前偷偷做了个动作,那就是将当前线程的同步上下文(SychronizationContext)给捕获了。
- 返回了一个未完成的任务,这里面抽象为Task
然后在第4步,当button1_Click
中去获取上面这个Task返回值的时候出现了死锁,button1_Click
和GetContentAsync
相互等待:
var content = task.Result;
button1_Click等待任务await http.GetStringAsync("http://www.imzjy.com");
完成- 而
await http.GetStringAsync("http://www.imzjy.com");
等待当前线程(UI线程)的同步上下文SychronizationContext
由于上面两个方法相互等待,所以产生了死锁。
为什么自动捕获当前线程同步上下文
GetContentAsync
自动捕获的是当前UI线程的同步上下文,通过偷偷的
捕获当前UI线程的同步上下文可以让你在GetContentAsync
方法中await
之后可以更新UI控件。如果你在GetContentAsync
中不需要更新UI控件,那么我们就不必捕获同步上下文,那么也就不存在这个问题。
解决方案 1
修改GetContentAsync,让http.GetStringAsync("http://www.imzjy.com”);
自动捕获上下文时候捕获不到。破坏了上面的死锁条件2。
private void button1_Click(object sender, EventArgs e)
{
var task = GetContentAsync();
var content = task.Result;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var syncContext = WindowsFormsSynchronizationContext.Current; //save SynchronizationContext
WindowsFormsSynchronizationContext.SetSynchronizationContext(null); //set SynchronizationContext to null
var http = new HttpClient();
var result = await http.GetStringAsync("http://www.imzjy.com");
WindowsFormsSynchronizationContext.SetSynchronizationContext(syncContext); //restore the SynchronizationContext
var first50 = result.Substring(0, 50);
return first50;
}
优点:
1. 调用方代码不需要改变
缺点:
1. 调用者线程(UI线程)会在var content = task.Result;
阻塞,直到GetContentAsync
返回,导致界面在此期间无响应。
2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")
需要更新界面(使用UI线程)会出现问题
3. 改的代码比较多3行。
4. WindowsFormsSynchronizationContext.SetSynchronizationContext(null);可能有副作用。
解决方案 2
修改调用方式,将调用放到Thread pool中,这样await http.GetStringAsync("http://www.imzjy.com");
的auto capture就不会获取到当前UI线程的SynchronizationContext
,破坏了上面的死锁条件2。
private void button1_Click(object sender, EventArgs e)
{
//put the GetContentAsync into thread pool, so that
//http.GetStringAsync("http://www.imzjy.com");
//will capture the SynchronizationContext from thread pool's excection environemnt
var task = Task<string>.Run(GetContentAsync);
var content = task.Result;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var http = new HttpClient();
var result = await http.GetStringAsync("http://www.imzjy.com");
var first50 = result.Substring(0, 50);
return first50;
}
优点:
1. async
方法不需要改变。
缺点:
1. 调用者线程(UI线程)会在var content = task.Result;
阻塞,直到GetContentAsync
返回,导致界面在此期间无响应。
2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")
需要更新界面(使用UI线程)会出现问题
解决方案 3
通过ConfigureAwait来改变自动捕获SynchronizationContext行为,破坏了上面的死锁条件2。
private void button1_Click(object sender, EventArgs e)
{
var task = GetContentAsync();
var content = task.Result;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var http = new HttpClient();
//tell await not to capture SynchronizationContext
var result = await http.GetStringAsync("http://www.imzjy.com”)
.ConfigureAwait(continueOnCapturedContext: false);
var first50 = result.Substring(0, 50);
return first50;
}
优点:
1. 调用方(caller)不需要改变
2. 避免了此处无用的自动捕获线程上下文。
缺点:
1. 调用者线程(UI线程)会在var content = task.Result;
阻塞,直到GetContentAsync
返回,导致界面在此期间无响应。
2. 如果异步方法类似http.GetStringAsync("http://www.imzjy.com")
需要更新界面(使用UI线程)会出现问题
解决方案 4
把当前的事件处理函数也改成async的,这样破坏了死锁条件的1。button1_Click
不在死等,所以也释放了上下文。
private async void button1_Click(object sender, EventArgs e)
{
var task = GetContentAsync();
var content = await task;
this.label1.Text = content;
}
public async Task<string> GetContentAsync()
{
var http = new HttpClient();
var result = await http.GetStringAsync("http://www.imzjy.com");
var first50 = result.Substring(0, 50);
textBox1.Text = first50;
return first50;
}
优点:
1. async
方法不需要改变。
2. 避免了UI无响应的问题。
3. GetContentAsync
在await
之后可以更新UI界面。
缺点:
1. button1_Click
改为了异步,对原来的方法有侵入性,甚至会改变整个调用链的行为,我最讨厌这点了。
上面的死锁通常会发生在下面两个地方
- Windows Forms的UI线程中调用了异步的方法。
- ASP.NET的User Request Context执行环境,比如Controller中的方法。 代码细节
异步方法实现者
- 分开提供同步和异步方法
- 只是自己做一些事,不需要bind到调用线程上的需要尽量
.ConfigureAwait(continueOnCapturedContext:false)
对于异步方法使用者
- 看看是否提供了同步方法
- 考虑是否有机会将自己的代码转为异步代码
- 实在不行放到threadpool中去执行
- 即使你调用的类库的实现者使用了
ConfigureAwait(false)
,但是你如果用Task
包装了一下,这时候需要在返回Task
对象上显示的调用ConfigureAwait(false)
,否则调用你包装代码的地方也可能发生死锁。因为你的包装方法会默认捕获当前线程的同步上下文(SychronizationContext)。
async/await中的异常处理
如果加上异常处理,那么async/await
会变得更加复杂,因为异步方法在异步执行,所以可以放到不同的线程中,那么如果出现了异常会怎么样?简单来说:
- 异步代码中的异常如果存在
Task
或Task<T>
被attach到了Task对象上 - 但是
async void
例外,由于没有Task对象可以attach,所以attach到了SynchronizationContext
中活跃的线程中了。 - 异步方法调用(调用链)中的异常,会被Aggregate,然后生成一个
AggregateException
。你可以使用aggExp.Flatten()
方法来方便查看这个调用链中所有异常--如果有多个的话。
完整代码示例
Recommend
-
82
-
19
-
24
-
22
闲话:小青蛙很久没发文,不是小孩子挂了,是小孩子正在着急改稿子,准备出《MySQL是怎样运行的: 从根儿上理解MySQL》的纸质书,在小册的基础上增删了一个半月,上周日终于改完了。 这本破书从去年三月...
-
54
这篇文章主要讲的是如何通过调试 MySQL 源码,知道一条 SQL 真正会拿哪些锁,不再抓虾,瞎猜或者何登成大神没写过的场景就不知道如何处理了 通过好多个深夜艰难的单步调试,终于找到了一个理想的断点,可以看到大部分获取锁的过程 代码在lock0lock.c的s
-
30
前言 前几天跟一位朋友分析了一个死锁问题,所以有了这篇图文详细的博文,哈哈~ 发生死锁了,如何排查和解决呢?...
-
10
Oozie任务死锁解决方案 Oozie是Apache下面的一个用于流程调度(workflow scheduler)的系统,主要用于管理Hadoop生态圈中的各种任务,目前支持丰富的任务类型:Jav...
-
5
Mr.Feng BlogNLP、深度学习、机器学习、Python、Go死锁问题:简单的解决方案介绍一种简单的避免死锁的加锁机制,并用Python实现和演示。 出现死锁的代码...
-
10
一个 MySQL 数据库死锁的案例和解决方案 2023/08/31 Database 共 1152 字,约 4 分钟 ...
-
9
在DBS-集群列表-更多-连接查询-死锁中,看到9月22日有数据库死锁日志,后排查发现是因为mysql的优化-index merge(索引合并)导致数据库死锁。 index merge(索引合并):该数据库查询优化的一种技术,在mysql 5.1之后进行引入,它可以在多个索引上进行查询,...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK