死锁示例
如果你开发一个简单的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()
方法来方便查看这个调用链中所有异常--如果有多个的话。
Comments: