3

我正在尝试使用实体框架 6(代码优先)+ WPF 进行异步等待编程,但我不明白为什么在我使代码异步后 UI 仍然冻结。这是我从第一行开始做的事情:

首先有一个响应点击按钮的事件处理程序:

private async void LoginButton_Click(object sender, RoutedEventArgs e) {
  if (await this._service.Authenticate(username.Text, password.Password) != null)
    this.Close();
}

然后我的服务层中有 Authenticate 方法:

public async Task<User> Authenticate(string username, string password) {
  CurrentUser = await this._context.GetUserAsync(username.ToLower().Trim(), password.EncryptPassword());
  return CurrentUser;
}

最后是上下文中的 EF 代码:

public async Task<User> GetUserAsync(string username, string password) {
  return await this.People.AsNoTracking().OfType<User>().FirstOrDefaultAsync(u => u.Username == username && u.Password == password);
}

更新:经过一些跟踪,UI冻结的原因原来是初始化过程。UI 线程阻塞,直到 EF 上下文被初始化,一旦完成,实际的查询/保存过程就会异步执行。

在单击处理程序开始处调用 Task.Yield() 后更新调试输出:

53:36:378 Calling Task.Yield
53:36:399 Called Task.Yield
53:36:400 awaiting for AuthenticateAsync
53:36:403 awaiting for GetUserAsync
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_32\System.Transactions\v4.0_4.0.0.0__b77a5c561934e089\System.Transactions.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Numerics\v4.0_4.0.0.0__b77a5c561934e089\System.Numerics.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_32\System.Data.OracleClient\v4.0_4.0.0.0__b77a5c561934e089\System.Data.OracleClient.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'D:\SkyDrive\Works\MyApp\MyApp.UI.WPF.Shell\bin\Debug\EntityFramework.SqlServer.dll'
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_32\System.EnterpriseServices\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.EnterpriseServices.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_32\System.EnterpriseServices\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.EnterpriseServices.Wrapper.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Runtime.Serialization\v4.0_4.0.0.0__b77a5c561934e089\System.Runtime.Serialization.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'EntityFrameworkDynamicProxies-MyApp.Model.Domain.People'
'MyApp.vshost.exe' (Managed (v4.0.30319)): Loaded 'EntityFrameworkDynamicProxies-MyApp.Model.Domain.Security'
53:39:965 Out of GetUserAsync
53:39:968 out of AuthenticateAsync
The thread '<No Name>' (0x1e98) has exited with code 0 (0x0).
The thread '<No Name>' (0x17d4) has exited with code 0 (0x0).
The thread '<No Name>' (0x175c) has exited with code 0 (0x0).
The thread '<No Name>' (0x220) has exited with code 0 (0x0).
The thread '<No Name>' (0x1dc8) has exited with code 0 (0x0).
The thread '<No Name>' (0x1af8) has exited with code 0 (0x0).
4

1 回答 1

4

标记为“异步”的方法在第一次“等待”发生之前仍然是同步的。因此,如果该初始代码中发生的任何事情花费的时间太长(我认为 WinRT 的指导方针是 200 毫秒或更多,这似乎是合理的),那么您可能希望通过提前插入等待来强制代码更快地返回。

例如,在您的 LoginButton_Click 中,您可以插入第一行“await Task.Yield ()”,这将允许调用更快地返回 UI 线程。

现在,仅通过该更改,由于异步/等待行为,这些方法仍将在 UI 线程上运行。我仍然喜欢首先进行更改,因为在许多情况下,这是用户实际期望发生的事情('async' 修饰符在这方面有点令人困惑),并且您可以在处理程序开始时执行此操作而不必搞砸随着事情的发展。

如果上述内容不足(例如上下文初始化花费的时间太长,仍然发生在 UI 线程上,并且仍然冻结 UI,只是在稍微不同的时间点),我们可以做的下一步是采取那些不这样做的部分不需要在 UI 线程上发生,并让 await 知道它们可以在任何线程上处理,而不仅仅是 UI 线程。对于响应能力而言,这通常是一个很好的做法,即使在代码当前运行“足够快”而不会成为明显问题的情况下也是如此。

为此,我们使用将ConfigureAwait (false) 添加到任务。

  • GetUserAsync 方法应该在 FirstOrDefaultAsync 调用之后添加它(“链”它)
    • 或者,恕我直言,稍微干净一点,就是去掉 GetUserAsync 方法中的 async/await 关键字,只返回从 FirstOrDefaultAsync 返回的任务。在这种方法中,异步/等待并没有真正“购买”你任何东西,恕我直言:)
  • 在 Authenticate 中,您可能应该在 GetUserAsync 调用之后添加它
    • 我不确定的一个潜在“问题”是 CurrentUser 是否数据绑定到 UI 中。由于它是 _service 的成员,我猜它不是,但即使是,我认为WPF 可以很好地处理在非 UI 线程上更新的数据绑定项,并且它处理将更改封送回 UI (调度员?)线程。这与 Silverlight 等框架不同,在 Silverlight 等框架中,更新数据绑定到 UI 的非 UI 上的属性将导致与手动更新目标控件相同的跨线程故障。如果我对此有误,并且 1) CurrentUser 是数据绑定到您的 UI2) 非 UI 线程上的数据绑定更新导致运行时异常,则避免在此方法中添加 ConfigureAwait(false)。很抱歉这篇文章的长度,只是想把我对这个特定修改有点不确定的地方联系起来。:)
  • 在 LoginButton_Click 中,我们不应该添加它,因为方法的其余部分 (this.Close) 需要在 UI 线程上发生,而这里的 ConfigureAwait(false) 会破坏它

一旦完成这两项更改,您将 1) 尽快将控制权返回给调用者(与事件处理程序同步的代码量最少)和 2) 做不需要的工作在其他线程上的 UI 线程上,这应该意味着您的 UI 不再“冻结”。

如果在这些更改之后它仍然冻结,您可能只需要在调试器下运行它,当它冻结时,break 以查看 UI 线程的堆栈是什么来找到有问题的代码。:)

祝你好运!

于 2012-11-07T13:37:19.393 回答