可能导致死锁的场景
async
/await
与 SynchronizationContext
结合时:
- 在WinForms或WPF应用程序中,
SynchronizationContext
会捕获当前线程的上下文。假设在UI线程上启动一个异步操作,该异步操作在await
处暂停,然后当await
后的代码恢复执行时,它会尝试在原始的同步上下文(即UI线程)上执行。如果此时UI线程被阻塞,就会导致死锁。例如,当异步方法中await
一个任务,而这个任务又需要获取UI线程上的资源(比如更新UI控件),同时UI线程正在等待这个异步任务完成,就会形成死锁。
- 代码示例:
private async void Button_Click(object sender, EventArgs e)
{
// 在UI线程上开始
var task = Task.Run(() =>
{
// 模拟一些工作
Thread.Sleep(2000);
// 这里尝试获取UI线程上的资源,比如更新UI
// 假设这里有一个需要在UI线程执行的方法UpdateUI()
// 但此时UI线程可能正在等待下面的await完成
// 会导致死锁
});
await task;
}
- 混合使用同步和异步代码:
- 当在异步方法中调用同步阻塞方法,而同步阻塞方法又等待异步操作完成时,可能会导致死锁。例如,在
async
方法中使用Task.Wait()
或Task.Result
来等待一个任务完成,而这个任务可能需要回到调用Wait()
或Result
的线程上下文来执行后续代码,就会形成死锁。
- 代码示例:
public async Task DoAsyncWork()
{
var task = Task.Run(() =>
{
// 模拟工作
Thread.Sleep(2000);
return 42;
});
// 错误的做法,会导致死锁
int result = task.Result;
// 或者 task.Wait();
}
避免死锁的方法
- 避免在异步代码中使用同步阻塞操作:
- 不要在
async
方法中使用Task.Wait()
或Task.Result
,而是始终使用await
来等待任务完成。
- 例如,将上述代码修改为:
public async Task DoAsyncWork()
{
var task = Task.Run(() =>
{
// 模拟工作
Thread.Sleep(2000);
return 42;
});
int result = await task;
}
- 正确处理同步上下文:
- 在WinForms或WPF应用程序中,可以使用
ConfigureAwait(false)
来避免在await
后恢复到原始的同步上下文。这样可以允许await
后的代码在ThreadPool线程上执行,而不是一定回到UI线程。例如:
private async void Button_Click(object sender, EventArgs e)
{
var task = Task.Run(() =>
{
// 模拟一些工作
Thread.Sleep(2000);
return 42;
});
int result = await task.ConfigureAwait(false);
// 如果需要更新UI,使用BeginInvoke或Dispatcher.Invoke回到UI线程
// 例如在WinForms中:
this.BeginInvoke((Action)(() =>
{
// 更新UI代码
label1.Text = result.ToString();
}));
}
- 这样可以防止因为UI线程等待异步任务,而异步任务又等待UI线程导致的死锁。