EF 6 中的异步等待问题

Posted

技术标签:

【中文标题】EF 6 中的异步等待问题【英文标题】:async-await issue in EF 6 【发布时间】:2012-10-26 06:59:06 【问题描述】:

我正在尝试使用实体框架 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).

【问题讨论】:

据我所知,EF 6.0 尚未发布,因此基于任务的异步支持尚未完全实现。 还取决于您的底层提供商是否支持它。如果不是,它会退回到阻塞状态。 不相关,但你可以在 GetUserAsync 中使用 OfType() 吗? 上下文初始化在 UI 冻结方面有多糟糕?您可以通过在应用程序启动时强制初始化来解决这个问题吗? (每个应用程序域应该只慢一次。) 嗯,它是一个模块化的应用程序。 7 个模块,每个模块都有自己的域。我认为每个域大约需要 8 秒。我会在启动时称之为坏事件。 【参考方案1】:

标记为“异步”的方法在第一次“等待”发生之前仍然是同步的。因此,如果该初始代码中发生的任何事情花费的时间太长(我认为 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 数据绑定到您的 UI 2) 非 UI 线程上的数据绑定更新会导致运行时异常,那么请避免 ConfigureAwait (false) 在此方法中添加。很抱歉这篇文章的长度,只是想把我对这个特定修改有点不确定的地方联系起来。 :) 在 LoginButton_Click 中,我们应该添加它,因为方法的其余部分 (this.Close) 需要在 UI 线程上发生,而此处的 ConfigureAwait(false) 会破坏它

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

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

祝你好运!

【讨论】:

而不是 Task.Yield 这让我走上了真正解耦 GUI 线程的解决方案,我必须这样做:await Task.Delay(TimeSpan.FromMilliseconds(1)).ConfigureAwait(false) ;强制屈服,但也继续在不同的线程上。我不能告诉 task.yield 从现在开始它应该使用不同的线程。我不禁想知道是否有比基本上延迟任意低的更好的方法

以上是关于EF 6 中的异步等待问题的主要内容,如果未能解决你的问题,请参考以下文章

与 MVC 3、.NET 4.5 和 EF 6 并行获取数据

EF Core - 异步函数阻塞 UI

ASPNet Entity Framework 6 - EF6,在同一个工作单元中混合异步和同步

在 webapi 中使用异步等待的最佳实践

实体框架 6 alpha 2 - 异步模式

EF学习笔记:异步处理和存储过程