以线程安全的方式使用 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 进行异步搜索的主要内容,如果未能解决你的问题,请参考以下文章