以线程安全的方式使用 DbContext 进行异步搜索

Posted

技术标签:

【中文标题】以线程安全的方式使用 DbContext 进行异步搜索【英文标题】:Async searching with DbContext in a thread-safe way 【发布时间】:2021-08-19 17:42:48 【问题描述】:

我正在使用带有 Azure SQL Server 和 C#/WPF 前端的 EFCore 5。我正在使用 Fody 来触发我的 PropertyChanged 事件。

我有一个自定义的实时搜索控件,其中包含一个包含搜索字符串的文本框和一个显示结果的列表框。

TextBox.Text 绑定到字符串属性“SearchString”。我订阅了 PropertyChanged,当它被触发时,我调用我的存储库类来执行异步数据库搜索(实际上在查询中我还有一些 .Where 和 .Include 子句,但已经提取了相关部分):

result = await Context.Where(p => (p.Surname == surname)
    .OrderBy(p => p.Surname)
    .Take(maxResults / sString.Length).ToListAsync();

return result;

然后,我将生成的列表放入同一个类中的一个属性中,该属性绑定到一个 ListBox.ItemsSource 属性。到目前为止,这在同步方面运行良好(但速度很慢)——我现在只是切换到异步。

我的问题是,尽管“等待”每个对数据库的异步调用,但如果我输入得太快,我会在 DbContext 类上得到“在前一个操作完成之前在此上下文上启动了第二个操作”。

我最好的猜测是,因为我对数据库存储库的搜索功能的初始调用包含在一个事件中,并且该事件在 UI 线程中触发,所以它“覆盖”了我正在使用 await 并导致第二次查询运行。我要么需要一种停止第一个查询的方法,要么需要一种确保等待得到尊重的方法。

有什么建议可以让 ToListAsync() 很好地工作,但让 UI 感觉响应灵敏?

【问题讨论】:

您应该为用户在 UI 中键入内容“消除抖动”事件,这样您就不会针对每次击键发出查询。这是一篇关于它的文章:weblog.west-wind.com/posts/2017/jul/02/… 也许您可以使用取消令牌。 去抖动系统似乎正是我正在寻找的,@MartinCostello。我通过在微控制器上的一些工作熟悉了这个概念,但没有在 Windows 桌面应用程序上考虑过它! 不过,问题是您的代码允许多线程访问一个上下文实例。您应该始终防止这种情况发生,而不是解决它。去抖动机制并不能保证调用是串行的。 @GertArnold 同意这样会更好。那么任何想法如何做到这一点?我想实际上是在显式创建的线程上进行工作,然后取消该线程是一种可能性。您可以看到仍然允许使用 async / await 的任何其他可能性? 【参考方案1】:

我订阅了 PropertyChanged,当它被触发时,我调用我的存储库类来执行异步数据库搜索......尽管“等待”每次对数据库的异步调用,但如果我输入得太快,我会得到“第二个操作在 DbContext 类上的上一个操作完成之前在此上下文中启动”。

我最好的猜测是,因为我对 DB 存储库的搜索功能的初始调用包含在一个事件中,并且该事件在 UI 线程中触发,所以它“覆盖”了我正在使用 await 并导致第二次查询运行。我要么需要一种停止第一个查询的方法,要么需要一种确保等待得到尊重的方法。

这是problems with async void 之一:调用代码很难知道异步方法何时完成。真正导致问题的是async void PropertyChanged

有什么建议可以让 ToListAsync() 很好地工作,但让 UI 感觉响应灵敏?

首先,我建议像其他人评论的那样去抖动。这将减少不必要的数据库调用,尽管它不会解决这个问题。要进行适当的修复,您应该有一个数据库上下文池,代码从中分配(并在操作完成后返回),或者只使用SemaphoreSlim。如果您有一个共享的SemaphoreSlim(与您现有的Context 所在的位置相同),并在db 操作之前调用await semaphore.WaitAsync();,在db 操作之后调用semaphore.Release(),则可以确保一次访问:

await ContextMutex.WaitAsync();
try

  result = await Context.Where(p => (p.Surname == surname)
      .OrderBy(p => p.Surname)
      .Take(maxResults / sString.Length).ToListAsync();

finally

  ContextMutex.Release();

我现在只是切换到异步。

SemaphoreSlim 的一个很好的特性是它也适用于同步代码;您可以让该代码调用Wait 而不是await WaitAsync。您应该对Context所有访问应用互斥以确保正确性。

【讨论】:

另一个想法可能是:如果信号量可以立即获取,则执行该工作,否则跳过该工作。 if (semaphore.Wait(0)) try /*...*/ finally semaphore.Release(); 非常感谢@stephen-cleary!正如我所说,我才刚刚开始使用异步,我相信您建议的答案将在未来帮助我进行大量编码。

以上是关于以线程安全的方式使用 DbContext 进行异步搜索的主要内容,如果未能解决你的问题,请参考以下文章

跨越异步边界的布尔标志的线程安全

EF DbContext 并发执行时可能出现的问题

实体框架:如何防止 dbcontext 被多个线程访问?

Nopcommerce 使用Task时dbcontext关闭问题

异步代码、共享变量、线程池线程和线程安全

并发与并行同步与异步,线程安全的实现