In this blog post we will have a look into how to disable the thread safety check in Entity Framework. What are the implications of doing so and how to do it.
Thread safety check in Entity Framework
DbContext
by default is not thread safe. Therefore the DbContext
tries to detect whether or not it is being used in a thread safe manner. Imagine the following code:
using var context = new MyDbContext();
var task1 = context.Users.ToListAsync();
var task2 = context.BlogPosts.ToListAsync();
await Task.WhenAll(task1, task2);
This will throw an exception because the DbContext
is being used in two different threads at the same time. If you want to do the above, you have to resort to the IDbContextFactory
to create a new DbContext
for each task:
public MyService(IDbContextFactory<MyDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
var dbContext1 = _dbContextFactory.CreateDbContext();
var dbContext2 = _dbContextFactory.CreateDbContext();
var task1 = dbContext1.Users.ToListAsync();
var task2 = dbContext2.BlogPosts.ToListAsync();
await Task.WhenAll(task1, task2);
Disabling the thread safety check
That check comes with a performance "penalty" (that is very minor). If you want to disable the thread safety check, you can do so by setting the DbContextOptionsBuilder.EnableThreadSafetyChecks
property to false
:
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite("Data Source=mydatabase.db")
.EnableThreadSafetyChecks(false);
Word of caution here: Doing this will disable the thread safety check and you have to be 100% sure that you are not using the DbContext
in a different thread at the same time.
For example by using the IDbContextFactory
to create a new DbContext
for each task as shown above that makes the DbContext
thread safe.
If you fail to ensure that, you will run into hard-to-debug issues that can lead to data corruption or other unexpected behavior in your application. (I will put this into the resources section down below).
The small upside is: Performance gain. Let's have a check here (code is also at the end of the post):
[MemoryDiagnoser]
public class EntityFrameworkBenchmark
{
private const int Iterations = 1000;
private TestDbContextWithThreadSafety? _dbContextWithThreadSafety;
private TestDbContextWithoutThreadSafety? _dbContextWithoutThreadSafety;
[GlobalSetup]
public void Setup()
{
_dbContextWithThreadSafety = new TestDbContextWithThreadSafety();
_dbContextWithThreadSafety.Database.EnsureCreated();
_dbContextWithoutThreadSafety = new TestDbContextWithoutThreadSafety();
_dbContextWithoutThreadSafety.Database.EnsureCreated();
}
[GlobalCleanup]
public async Task Cleanup()
{
if (_dbContextWithThreadSafety is not null)
{
await _dbContextWithThreadSafety.DisposeAsync();
}
if (_dbContextWithoutThreadSafety is not null)
{
await _dbContextWithoutThreadSafety.DisposeAsync();
}
}
[Benchmark(Baseline = true)]
public async Task<List<TestEntity>> WithThreadSafetyChecks()
{
var results = new List<TestEntity>();
for (var i = 0; i < Iterations; i++)
{
results.AddRange(await _dbContextWithThreadSafety!.TestEntities.ToListAsync());
}
return results;
}
[Benchmark]
public async Task<List<TestEntity>> WithoutThreadSafetyChecks()
{
var results = new List<TestEntity>();
for (var i = 0; i < Iterations; i++)
{
results.AddRange(await _dbContextWithoutThreadSafety!.TestEntities.ToListAsync());
}
return results;
}
}
And here are the results:
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|-------------------------- |----------:|----------:|----------:|------:|--------:|---------:|---------:|----------:|------------:|
| WithThreadSafetyChecks | 10.090 ms | 0.1381 ms | 0.1224 ms | 1.00 | 0.02 | 875.0000 | 437.5000 | 6.98 MB | 1.00 |
| WithoutThreadSafetyChecks | 9.423 ms | 0.1332 ms | 0.1112 ms | 0.93 | 0.02 | 843.7500 | 421.8750 | 6.8 MB | 0.97 |
A very small gain - keep in mind that there is exactly 0 data coming back from the database, so this is just a pure performance test of the DbContext
itself.
If we would have data coming back from the database, the performance gain would be even smaller (relative to the overall runtime)!
So does it make sense to use that? Well, as a very advanced use case, maybe. But using something like a pooled IDbContextFactory
is a much better option in my opinion before resorting to this.
Resources
- Official Microsoft Documentation for Entity Framework: https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-di%2Cexpression-api-with-constant
- Source code for this blog post: https://github.com/linkdotnet/BlogExamples/tree/main/EFDisableThreadSafety
- Source code for many of my blog posts: https://github.com/linkdotnet/BlogExamples