Ef Data Context - Async/Await & Multithreading

EF Data Context - Async/Await & Multithreading

We have a stalemate situation here. AspNetSynchronizationContext, which is responsible for the threading model of an ASP.NET Web API execution environment, does not guarantee that asynchronous continuation after await will take place on the same thread. The whole idea of this is to make ASP.NET apps more scalable, so less threads from ThreadPool are blocked with pending synchronous operations.

However, the DataContext class (part of LINQ to SQL )
is not thread-safe, so it shouldn't be used where a thread switch may potentially occurr across DataContext API calls. A separate using construct per asynchronous call will not help, either:

var something;
using (var dataContext = new DataContext())
{
something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
}

That's because DataContext.Dispose might be executed on a different thread from the one the object was originally created on, and this is not something DataContext would expect.

If you like to stick with the DataContext API, calling it synchronously appears to be the only feasible option. I'm not sure if that statement should be extended to the whole EF API, but I suppose any child objects created with DataContext API are probably not thread-safe, either. Thus, in ASP.NET their using scope should be limited to that of between two adjacent await calls.

It might be tempting to offload a bunch of synchronous DataContext calls to a separate thread with await Task.Run(() => { /* do DataContext stuff here */ }). However, that'd be a known anti-pattern, especially in the context of ASP.NET where it might only hurt performance and scalability, as it would not reduce the number of threads required to fulfill the request.

Unfortunately, while the asynchronous architecture of ASP.NET is great, it remains being incompatible with some established APIs and patterns (e.g., here is a similar case).
That's especially sad, because we're not dealing with concurrent API access here, i.e. no more than one thread is trying to access a DataContext object at the same time.

Hopefully, Microsoft will address that in the future versions of the Framework.

[UPDATE] On a large scale though, it might be possible to offload the EF logic to a separate process (run as a WCF service) which would provide a thread-safe async API to the ASP.NET client logic. Such process can be orchestrated with a custom synchronization context as an event machine, similar to Node.js. It may even run a pool of Node.js-like apartments, each apartment maintaining the thread affinity for EF objects. That would allow to still benefit from the async EF API.

[UPDATE] Here is some attempt to find a solution to this problem.

EF6 Async Methods Confusion

From the same EF docs you quoted:

For the moment, EF will detect if the developer attempts to execute
two async operations at one time and throw.

So, this code should work even if there's a thread switch after await, because it's still executed sequentially:

var dbContext = new DbContext();
var something = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
var morething = await dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);

At least, this is how it is expected to work. If sequential execution still produces a threading-related exception, this should be reported as an EF bug.

On the other hand, the following code will most likely fail, because we introduce parallelism:

var dbContext = new DbContext();
var somethingTask = dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
var morethingTask = dbContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);

await Task.WhenAll(somethingTask, morethingTask);

var something = somethingTask.Result;
var morething = morethingTask.Result;

You need to make sure you don't use the same DbContext with more than one pending EF operation.

Updated, the 1st code fragment actually works fine with EF v6.1.0, as expected.

Async searching with DbContext in a thread-safe way

I subscribe to PropertyChanged and when this is fired, I call my repository classes to perform an async database search... despite "await"ing every asynchronous call to the database, if I type too quickly I get "A second operation was started on this context before a previous operation completed" on the DbContext class.

My best guess is that because my initial call to the DB Repository's search function is contained in an event, and that event is firing in the UI thread, it's "Overriding" the fact that I'm using await and causing a second query to run. I either need a way of stopping the first query, or a way of ensuring that the await is honoured.

This is one of the problems with async void: it's not easy for calling code to know when the asynchronous method has completed. It's the async void PropertyChanged that is actually causing the problem.

Any suggestions for making this work well with ToListAsync(), but sill allowing the UI to feel responsive?

First, I recommend debouncing as others have commented. That will reduce unnecessary db calls, although it won't fix this problem. For a proper fix, you should either have a pool of db contexts that your code allocates from (and returns to after the operation is complete), or just use a SemaphoreSlim. If you have one shared SemaphoreSlim (the same place where your existing Context is), and call await semaphore.WaitAsync(); before the db operations and semaphore.Release() after the db operations, then that ensures one-at-a-time access:

await ContextMutex.WaitAsync();
try
{
result = await Context.Where(p => (p.Surname == surname)
.OrderBy(p => p.Surname)
.Take(maxResults / sString.Length).ToListAsync();
}
finally
{
ContextMutex.Release();
}

I am just switching over to async now.

One nice feature of SemaphoreSlim is that it also works with synchronous code; you can have that code call Wait instead of await WaitAsync. You should apply the mutual exclusion to all accesses of Context to ensure correctness.

DbContext & DbcontextPool in ef-core

I understand why you think the language in the Microsoft documents is confusing. I'll unravel it for you:

  • "DbContext is not thread-safe." This statement means that it's not safe to access a DbContext from multiple threads in parallel. The stack overflow answers you already referenced, explain this.
  • "Do not share contexts between threads." This statement is confusing, because asynchronous (async/await) operations have the tendency to run across multiple threads, although never in parallel. A simpler statement would be: "do not share contexts between web requests," because a single web request typically runs a single unit of work and although it might run its code asynchronously, it typically doesn't run its code in parallel.
  • "Dbcontext is safe from concurrent access issues in most ASP.NET Core applications": This text is a bit misleading, because it might make the reader believe that DbContext instances are thread-safe, but they aren't. What the writers mean to say here is that, with the default configuration (i.e. using AddDbContext<T>(), ASP.NET Core ensures that each request gets its own DbContext instance, making it, therefore, "safe from concurrent access" by default.

1 I was confused about whether registering DBcontext as a scoped service is thread-safe or not?

DbContext instances are by themselves not thread-safe, which is why you should register them as Scoped, because that would prevent them from being accessed from multiple requests, which would make their use thread-safe.

2 What are the problems of registering DBcontext as a singleton service in detail?

This is already described in detail in this answer, which you already referenced. I think that answer goes into a lot of detail, which I won't repeat here.

In addition, I read some docs that prohibit registering singleton DbContext, however, AddDbContextPool makes to register singleton DBcontext. so there are some questions about the Dbcontextpool.

The DbContext pooling feature is very different from registering DbContext as singleton, because:

  • The pooling mechanism ensures that parallel requests get their own DbContext instance.
  • Therefore, multiple DbContext instances exist with pooling, while only a single instance for the whole application exists when using the Singleton lifestyle.
  • Using the singleton lifestyle, therefore, ensures that one single instance is reused, which causes the myriad of problems laid out (again) here.
  • The pooling mechanism ensures that, when a DI scope ends, the DbContext is 'cleaned' and brought back to the pool, so it can be reused by a new request.

what are the impacts of using the DbContextPool instead of the DbContext?

More information about this is given in this document.

when we should use it and what should be considered when we use contextPool?

When your application requires the performance benefits that it brings. This is something you might want to benchmark before deciding to add it.

DbContextPool is thread-safe?

Yes, in the same way as registering a DbContext as Scoped is thread-safe; in case you accidentally hold on to a DbContext instance inside an object that is reused accross requests, this guarantee is broken. You have to take good care of Scoped objects to prevent them from becoming Captive Dependencies.

Has it memory issues because of storing a number of dbset instances throughout the application's lifetime?

The memory penalty will hardly ever be noticable. The so-called first-level cache is cleared for every DbContext that is brought back to the pool after a request ends. This is to prevent the DbContext from becoming stale and to prevent memory issues.

change-tracking or any parts of the ef would be failed or not in the DB context pool?

No, it doesn't. For the most part, making your DbContext pooled is something that only requires infrastructural changes (changes to the application's startup path) and is for the most part transparent to the rest of your application. But again, make sure to read this to familiar yourself with the consequences of using DbContext pooling.

Entity Framework causing issue using Multithreading (Task) in C#

Threads spawned with async methods usually return a Task or Task<TResult>. Unfortunately, the impersonation context of the caller is not passed along to this thread - by default. That may be why you can't connect to the DB - wrong credentials.

You can pass the impersonation context through aspnet.config file settings using the <alwaysFlowImpersonationPolicy> in IIS

OR

Within your code, use WindowsIdentity and WindowsImpersonationContextto allow Impersonation context to flow with the async thread.

Here are a few articles of interest:

http://blog.codeishard.net/2012/09/17/await-async-mvc-and-impersonation/

How do I set the user identity for tasks when calling Task.WaitAll()?



Related Topics



Leave a reply



Submit