问题描述
WPF界面中有一个按钮,点击时需要获取数据,并写入文件,问题出在每次点击后程序都会完全卡死,恢复不过来。精简代码如下:
C#
private string data = null;
private string GetData()
{
if (data == null)
{
try
{
var task = GetDataAsync();
task.Wait(2000);
data = task.Result;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
data = null;
}
}
return data;
}
private async Task<string> GetDataAsync()
{
var data = await Task.Run(() =>
{
System.Threading.Thread.Sleep(1000);
return "test";
});
return data;
}
private void btnClick(object sender, RoutedEventArgs e)
{
var data = GetData();
System.Diagnostics.Debug.WriteLine(data);
// do something
}
在UI线程中调用GetData
方法,等待task完成,这里必然会阻塞界面,但不应该一直卡死才对。在task
执行完或超时后,应该恢复界面才对。经调试发现卡死的地方出在 data = task.Result;
这里。
卡死原因
-
异步上下文的 "循环等待"
GetDataAsync()
中await Task.Run(...)
会默认捕获当前的 UI 同步上下文 (DispatcherSynchronizationContext
),这意味着await
之后的代码(return data
)需要回到 UI 线程执行。- 但此时 UI 线程正被
task.Wait(2000)
阻塞,等待GetDataAsync()
完成。 - 当
Task.Run
中的操作完成后,GetDataAsync()
需要回到 UI 线程执行后续代码,却发现 UI 线程被阻塞,无法处理这个上下文切换,导致 死锁。 - 死锁发生后,
task
始终处于未完成状态,task.Result
会无限等待,最终界面卡死。
-
超时机制失效
task.Wait(2000)
虽然设置了超时,但死锁导致task
永远不会进入 "完成" 状态,超时判断也会失效,无法打破阻塞。- 即使超时时间到,
task
仍处于WaitingForActivation
状态,此时访问task.Result
会继续阻塞,直到死锁被打破(实际上永远不会)。
解决方案
通过 ConfigureAwait(false)
可以让 GetDataAsync()
不依赖 UI 线程上下文,避免死锁。
js
private async Task<string> GetDataAsync()
{
var data = await Task.Run(() =>
{
System.Threading.Thread.Sleep(1000);
return "test";
}).ConfigureAwait(false); // 不捕获UI同步上下文
// 注意:此后的代码会在线程池线程执行,而非UI线程
return data;
}
原理说明
ConfigureAwait(false)
告诉await
不需要将后续代码切回原上下文(UI 线程),而是直接在线程池线程中执行return data
。- 这样
GetDataAsync()
可以独立完成,无需依赖被阻塞的 UI 线程,避免了死锁。 task.Wait(2000)
会在 1 秒后(Task.Run
完成)收到信号,completed
为true
,此时task.Result
可正常获取结果,不会阻塞。- 若
Task.Run
耗时超过 2 秒,completed
为false
,可进入超时处理逻辑,同样不会卡死。
总结
问题的核心是 UI 线程被 task.Wait()
同步阻塞 ,违背了异步编程的原则。通过解除异步上下文依赖,可彻底解决 task.Result
处的死锁问题,但是对于本例仍然会出现最大2秒的界面卡死情况。在 WPF 中,UI 线程的事件处理应遵循 "异步到底"(async all the way) 原则,使用 async/await
实现异步等待。
C#
private async void btnClick(object sender, RoutedEventArgs e)
{
var data = await Task.Run(() => GetData());
System.Diagnostics.Debug.WriteLine(data);
// 此处已回到UI线程,可安全更新UI。
}