C# await、UI和死锁的问题
开发人员对Asnync 异步机制的兴趣程度很高。当然,任何新技术都必然会出现一些小问题。
我现在多次看到的一个问题是开发人员通过阻止他们的 UI 线程意外地使他们的应用程序死锁,所以我认为花一些时间来探索这种情况的常见原因以及如何避免这种困境是值得的。
就其核心而言,新的异步语言功能旨在恢复开发人员编写他们习惯编写的顺序、命令式代码的能力,但使其本质上是异步的而不是同步的。
private void button1_Click(object sender, RoutedEventArgs e) { string s = LoadString(); textBox1.Text = s; }
public Task<string> LoadStringAsync();
此方法将非常快速地返回给它的调用者,返回一个 .NET Task<string> 对象,该对象表示异步操作的未来完成及其未来结果。
在未来某个时刻,当操作完成时,任务对象将能够分发操作的结果,在成功加载的情况下可以是字符串,在失败的情况下可以是异常。
无论哪种方式,任务对象都提供了几种机制来通知对象的持有者加载操作已完成。
一种方法是同步阻塞等待任务完成,这可以通过调用任务的 Wait 方法或访问它的 Result 来完成,这将隐式等待直到操作完成......在这两种情况下,在操作完成之前,不会完成对这些成员的调用。
另一种方法是接收异步回调,在那里您向任务注册一个委托,该委托将在任务完成时调用。
这可以使用 Task 的 ContinueWith 方法之一来完成。
使用 ContinueWith,我们现在可以重写之前的 button1_Click 方法,以便在异步等待加载操作完成时不阻塞 UI 线程:
private void button1_Click(object sender, RoutedEventArgs e) { Task<string> s = LoadStringAsync(); s.ContinueWith(delegate { textBox1.Text = s.Result; }); // 警告:有问题 }
这实际上是异步启动加载操作,然后在操作完成时异步运行代码以将结果存储到 UI 中。
然而,我们现在有一个新的问题。Windows 窗体、WPF 和 Silverlight 等 UI 框架都对哪些线程能够访问 UI 控件设置了限制,即只能从创建它的线程访问该控件。
然而,在这里,我们在某个任意线程上运行回调来更新 textBox1 的文本,无论 ContinueWith 的任务并行库 (TPL) 实现碰巧放置它。
为了解决这个问题,我们需要一些方法来回到 UI 线程。
不同的 UI 框架为此提供了不同的机制,但在 .NET 中它们都采用基本相同的形式,
private void button1_Click(object sender, RoutedEventArgs e) { Task<string> s = LoadStringAsync(); s.ContinueWith(delegate { Dispatcher.BeginInvoke(new Action(delegate { textBox1.Text = s.Result; })); }); }
.NET Framework 进一步抽象了这些返回到 UI 线程的机制,通常是通过 SynchronizationContext 类将一些代码发布到特定上下文的机制。
框架可以通过 SynchronizationContext.Current 属性建立当前上下文,该属性提供表示当前环境的 SynchronizationContext 实例。
此实例的 Post 方法会将委托编组回要调用的此环境:
在 WPF 应用程序中,这意味着将您带回之前所在的调度程序或 UI 线程。
因此,我们可以将之前的代码改写如下:
private void button1_Click(object sender, RoutedEventArgs e) { var sc = SynchronizationContext.Current; Task<string> s = LoadStringAsync(); s.ContinueWith(delegate { sc.Post(delegate { textBox1.Text = s.Result; }, null); }); }
事实上,这种模式非常普遍,.NET 4 中的 TPL 提供了 TaskScheduler.FromCurrentSynchronizationContext() 方法,它允许您使用以下代码做同样的事情:
private void button1_Click(object sender, RoutedEventArgs e) { LoadStringAsync().ContinueWith(s => textBox1.Text = s.Result, TaskScheduler.FromCurrentSynchronizationContext()); }
private void button1_Click(object sender, RoutedEventArgs e) { string s = await LoadStringAsync(); textBox1.Text = s; }
static async Task<string> LoadStringAsync() { string firstName = await GetFirstNameAsync(); string lastName = await GetLastNameAsync(); return firstName + " " + lastName;}
private void button1_Click(object sender, RoutedEventArgs e) { Task<string> s = LoadStringAsync(); textBox1.Text = s.Result; // 警告:有问题 }
private void button1_Click(object sender, RoutedEventArgs e) { var mre = new ManualResetEvent(false); SynchronizationContext.Current.Post(_ => mre.Set(), null); mre.WaitOne(); // 警告:有问题 }
Task<string> s = LoadStringAsync(); textBox1.Text = s.Result; // BAD ON UI
你可以写成:
Task<string> s = LoadStringAsync();textBox1.Text = await s; // GOOD ON UI
不要写成下面这样:
Task t = DoWork(); t.Wait(); // BAD ON UI
而是要写成这样:
Task t = DoWork(); await t; // GOOD ON UI