Before executing a query with pagination and a lot of filters, I call .CountAsync()
on it. It turned out that, for some filter conditions, .CountAsync()
takes a very long time to finish (because of necessary joins) and the advantage of having the number available is not that important that it is worth the long waiting time.
在执行一个带有分页和大量过滤器的查询之前,我调用.CountAsync()。事实证明,对于某些过滤器条件,.CountAsync()需要很长时间才能完成(因为必要的连接),并且拥有可用数量的优势并不重要,值得长时间等待。
Thus I decided to cancel the counting in those scenarios where it takes longer than one second and proceed with the actual query.
因此,我决定取消那些需要超过一秒时间的计数,并继续进行实际的查询。
var tokenSource = new CancellationTokenSource();
var ct = tokenSource.Token;
var totalCountTask = query.CountAsync(ct);
var finishedTask = await Task.WhenAny(totalCountTask, Task.Delay(1000));
var totalCount = 0;
if (finishedTask == totalCountTask)
{
totalCount = await totalCountTask;
}
else
{
tokenSource.Cancel();
}
items = await query
.Skip((filter.Page - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();
But in cases where the count operation is cancelled, the next query executions throws an exception:
但在取消计数操作的情况下,下一次查询执行会抛出一个异常:
System.InvalidOperationException: 'A second operation was started on this context instance before a previous operation completed. ...
But even though the task is cancelled successfully (I checked this in debug mode), it seems the previous query is still running in the background, preventing DbContext
to do other work.
但是,即使任务被成功取消(我在调试模式下检查了这一点),前面的查询似乎仍然在后台运行,从而阻止DbContext执行其他工作。
Is there any possibility to "free" the DbContext
after cancelling a query to be able to use it again?
在取消查询后,是否有可能“释放”DbContext以便能够再次使用它?
更多回答
After the query is aborted, it need to wait that abort is completed. Else, it's possible the second query start before the abort complete.
查询中止后,需要等待中止完成。否则,第二个查询可能在中止完成之前开始。
if (finishedTask == totalCountTask)
{
totalCount = await totalCountTask;
}
else
{
tokenSource.Cancel();
try
{
await totalCountTask;
}
catch // swallow the exception from the abort
{ }
}
The explanation is that a cancelled task isn't in a Cancelled
state immediately after cancelling the cancellation token.
其解释是,取消的任务在取消令牌后不会立即处于已取消状态。
A cancellation token modestly signals that cancellation is requested, it's in no way capable of effecting cancellation instantly. It's an application programmer's task to make a process listen to the cancelation request. They will always try to stop a process "gracefully". What that means differs per task, but think of disposing IDisposable
s, closing connections, etc., all aimed at preventing side effects. That will always take some time.
取消令牌只是谦虚地发出取消请求的信号,它根本不能立即取消。应用程序程序员的任务是让进程侦听取消请求。他们总是试图“优雅地”停止一个进程。这意味着每个任务不同,但请考虑处理IDisposable、关闭连接等,所有这些都是为了防止副作用。这总是需要一些时间。
This simplified version of your code is (nearly always) guaranteed to throw the InvalidOperationException
. When the commented line is commented out, it won't ever throw, because the task gets the (short) time it needs to abort.
这个简化版本的代码(几乎总是)保证会抛出InvalidOperationException异常。当注释行被注释掉时,它永远不会抛出,因为任务获得了(短的)中止所需的时间。
var query = dbContext.Set<SomeTable>();
var tokenSource = new CancellationTokenSource();
var ct = tokenSource.Token;
var totalCountTask = query.CountAsync(ct);
await Task.WhenAny(totalCountTask, Task.CompletedTask);
tokenSource.Cancel();
//while (!totalCountTask.IsCompleted); // This "fixes" the exception
query.Load();
Needless to say, this is bad code. Normally, it shouldn't be necessary to orchestrate parallel tasks. The fundamental flaw in your case is that a DbContext
instance is accessed by multiple threads. That should never ever happen. DbContext
is not thread-safe. Each task should have its own context instance.
不用说,这是糟糕的代码。正常情况下,应该没有必要编排并行任务。您的案例中的根本缺陷是DbContext实例被多个线程访问。这永远不应该发生。DbContext不是线程安全的。每个任务都应该有自己的上下文实例。
更多回答
Thanks a lot! This solves my problem and prevents from having to wait 6-8 seconds in some scenarios. Obvious, once I saw it :)
非常感谢!这解决了我的问题,避免了在某些情况下不得不等待6-8秒。很明显,我一看就知道:)
I don't see how swallowing an exception fixes the problem. It means that await totalCountTask
didn't even run. But it's strange anyway that if finishedTask == totalCountTask
you run totalCountTask
again. Probably it's an artifact of slimming down code for the sake of the question but it may also explain why you consider this a fix: in reality it doesn't matter if totalCountTask
runs again, or not.
我看不出吞下一个异常是如何解决问题的。这意味着await totalCountTask甚至没有运行。但奇怪的是,如果finishedTask == totalCountTask,你会再次运行totalCountTask。也许这是为了解决这个问题而精简代码的产物,但它也可以解释为什么你认为这是一个修复:实际上,totalCountTask是否再次运行并不重要。
@GertArnold It's waiting the task to finish that solve the problem. Because the task is aborted, it finish with a (expected) exception, that is swallowed.
@GertArnold正在等待解决问题的任务完成。因为任务被中止,所以它以一个(预期的)异常结束,该异常被吞噬。
@GertArnold, It's depend when the abort interfere. It's can be TaskCanceledException. If the query is running, it's the provider that throw. With Sql Server it's SqlException.
@GertArnold,这取决于中止何时干扰。它可以是TaskCanceledException。如果查询正在运行,则抛出的是提供程序。对于SQL Server,它是SqlException。
Thanks for you explanation. Should have been abvious to me directly, but sometimes... However, vernou was a little faster and the try/catch seems totally OK to me. I don't understand why you conclude the DbContext is accessed by multiple threads. The problem was of course, that the first parallel Task had not been completed before using the DbContext again, but I do not see where there is another thread.
谢谢你的解释。你应该直接告诉我的,但有时...然而,vernou稍微快一点,try/catch对我来说似乎完全可以。我不明白为什么你得出DbContext被多个线程访问的结论。当然,问题是,第一个并行任务在再次使用DbContext之前还没有完成,但我看不出哪里有另一个线程。
Well, frankly, I don't think that's a good fix, it just wipes the problem under the carpet. You do access the context from two threads, the one running totalCountTask
and the one running items = await query ...
. That's the problem and that must be fixed.
好吧,坦率地说,我不认为这是一个好的解决办法,它只是把问题掩盖了。您确实可以从两个线程访问上下文,一个运行totalCountTask,另一个运行Items=AWait Query...这就是问题所在,必须加以解决。
Each task should have its own context instance.
, not true. DbContext isn't thread safe, but it can be used in asynchronous way.
每个任务都应该有自己的上下文实例,不是这样的。DbContext不是线程安全的,但它可以以异步方式使用。
From the documentation EF Core - Asynchronous Programming : EF Core doesn't support multiple parallel operations being run on the same context instance. You should always wait for an operation to complete before beginning the next operation. This is typically done by using the await keyword on each async operation.
根据文档EF Core-异步编程:EF Core不支持在同一上下文实例上运行多个并行操作。在开始下一个操作之前,您应该始终等待操作完成。这通常是通过在每个异步操作上使用AWAIT关键字来完成的。
@vernou Here for some reason a deliberate decision is made to not await all code. Such code should never share context instances.
@Vernou在这里出于某种原因故意决定不等待所有代码。这样的代码永远不应该共享上下文实例。
我是一名优秀的程序员,十分优秀!