Preface
Whether it's in my personal blog or in my book , For the context instance pool, only a large number of text descriptions are used to explain its basic principle , And it's just a little bit , So we still know little about it , In this paper, we put the source code , Start with the source . Hope through this article from the source analysis , We can all see the difference between the upper injection and the context instance pool , When to use context , When to use context instance pool
Context instance pool principle preparation work
The concept of context instance pool and thread pool is the same , It's all reusable , But there are essential differences in principle implementation .EF Core Define the context instance pool interface, that is IDbContextPool, Abstract its interface implementation as : lease (Rent) And return (Return). as follows :
public interface IDbContextPool { DbContext Rent(); bool Return([NotNull] DbContext context); }
So what is the mechanism of leasing and returning ? Let's start with the injection context instance pool . When we're in Startup When context and context instance pool are injected into , We will ignore the other parameter configuration for the moment , The biggest difference between the two is that , The context can be customized to set the lifecycle , The default is Scope, The context instance pool can customize the maximum pool size , The default is 128. So here comes the question , What exactly is the lifecycle of the context managed by the context instance pool ? Let's explore the source code , In the parameter detail judgment part, the analysis is ignored here
private static void CheckContextConstructors<TContext>() where TContext : DbContext { var declaredConstructors = typeof(TContext).GetTypeInfo().DeclaredConstructors.ToList(); if (declaredConstructors.Count == 1 && declaredConstructors[0].GetParameters().Length == 0) { throw new ArgumentException(CoreStrings.DbContextMissingConstructor(typeof(TContext).ShortDisplayName())); } }
First, determine that the context must have a constructor , Because there is an implicit default parameterless constructor , So continue to enhance judgment , Constructor parameter cannot be 0, Otherwise, throw an exception
AddCoreServices<TContextImplementation>( serviceCollection, (sp, ob) => { optionsAction(sp, ob); var extension = (ob.Options.FindExtension<CoreOptionsExtension>() ?? new CoreOptionsExtension()) .WithMaxPoolSize(poolSize); ((IDbContextOptionsBuilderInfrastructure)ob).AddOrUpdateExtension(extension); },ServiceLifetime.Singleton );
secondly , Inject... In the form of a single example DbContextOptions, Because no matter how many times each context is instantiated , Its DbContextOptions No change
serviceCollection.TryAddSingleton( sp => new DbContextPool<TContextImplementation>( sp.GetService<DbContextOptions<TContextImplementation>>()));
then , Inject context instance pool interface in singleton form , Because there is a queue mechanism in this instance to maintain the context , All of these must be singletons , meanwhile , This example needs to use DbContextOptions, So early Injection DbContextOptions
serviceCollection.AddScoped<DbContextPool<TContextImplementation>.Lease>();
Then , Take the life cycle as Scope Inject Lease class , This class exists as a context instance pool nested sealed class , From the word understanding is to release the context ( The return ) Handle ( We will talk about )
serviceCollection.AddScoped(
sp => (TContextService)sp.GetService<DbContextPool<TContextImplementation>.Lease>().Context);
Last , Here is the context managed by the context instance pool , Its life cycle is Scope, Non modifiable
Context instance pool principle construction implementation
First, the important properties in context instance pool are given , So as not to look down
private const int DefaultPoolSize = 32; private readonly ConcurrentQueue<TContext> _pool = new ConcurrentQueue<TContext>(); private readonly Func<TContext> _activator; private int _maxSize; private int _count; private DbContextPoolConfigurationSnapshot _configurationSnapshot;
The above is the preparation for injection context instance pool , Next we come to the context instance pool implementation
public DbContextPool([NotNull] DbContextOptions options) { _maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize; options.Freeze(); _activator = CreateActivator(options); if (_activator == null) { throw new InvalidOperationException( CoreStrings.PoolingContextCtorError(typeof(TContext).ShortDisplayName())); } }
In its structure , Get the maximum size of the custom instance pool , If not set, use DefaultPoolSize Subject to ,DefaultPoolSize Defined as a constant 32, then , Prevent context instantiation after DbContextOptions Configuration changes , Call at this time Freeze Method to freeze , Next is the instantiation context , Now wrap it in the Commission , It's not really instantiated yet , Continue with the above CreateActivator Method realization .
private static Func<TContext> CreateActivator(DbContextOptions options) { var constructors = typeof(TContext).GetTypeInfo().DeclaredConstructors .Where(c => !c.IsStatic && c.IsPublic) .ToArray(); if (constructors.Length == 1) { var parameters = constructors[0].GetParameters(); if (parameters.Length == 1 && (parameters[0].ParameterType == typeof(DbContextOptions) || parameters[0].ParameterType == typeof(DbContextOptions<TContext>))) { return Expression.Lambda<Func<TContext>>( Expression.New(constructors[0], Expression.Constant(options))) .Compile(); } } return null; }
in short , Context constructors and parameters have and can only have one , And the parameter must be of type DbContextOptions, Finally through lambda Expression constructs context delegate . Through the above analysis , Under normal circumstances , We know that design is like this , Context can only be explicitly parameterized , And the parameter must have only one and must be DbContextOptions, But in some cases , We do need to use injection instances in context construction , Can't we play , If there is such a need , Here please refer to the previous article (EntityFramework Core 3.x Context constructors can inject instances ?)
The principle of context instance pool is essentially implemented
The context instance pool is constructed to get the maximum instance pool size and construct the context delegate ( Not really using ), The next step is to lease the context (Rent) And return (Return) Handle
public virtual TContext Rent() { if (_pool.TryDequeue(out var context)) { Interlocked.Decrement(ref _count); ((IDbContextPoolable)context).Resurrect(_configurationSnapshot); return context; } context = _activator(); ((IDbContextPoolable)context).SetPool(this); return context; }
Get the context from the queue in the context instance pool , Obviously , Not for the first time , The context delegate is activated , Instantiation context , If it exists, it will _count reduce 1, Then the state of the context is activated or revived ._count Property is used to get the instance pool size maxSize Compare ( As for how to compare , Next, return it to me ), Then to prevent concurrent thread interrupt and other mechanisms , You can't use simple _count--, It has to be atomic , So use Interlocked, I don't know the usage , Make up the foundation .
public virtual bool Return([NotNull] TContext context) { if (Interlocked.Increment(ref _count) <= _maxSize) { ((IDbContextPoolable)context).ResetState(); _pool.Enqueue(context); return true; } Interlocked.Decrement(ref _count); return false; }
When the context is released ( What to do with the release , Here can speak ), First reset the context state , It's just the model that the context is tracking ( Change tracking mechanism ) Close processing and so on , I don't want to go into it here , The next step is to return the context to the queue . We combine leasing and return as a whole : Set the pool size to 32, If there are 33 A request , And the processing time is longer , At this point, it will be leased directly 33 Context (s) , Then 33 Contexts are released in succession , At this point, it will be 0-31 Return to the queue , When the index is 32 when , here _count by 33, Can't join the team , How make ? At this time, it will come to inject Lease Class release processing
public TContext Context { get; private set; } void IDisposable.Dispose() { if (_contextPool != null) { if (!_contextPool.Return(Context)) { ((IDbContextPoolable)Context).SetPool(null); Context.Dispose(); } _contextPool = null; Context = null; } }
If the request exceeds the custom pool size , And the request processing cycle is very long , So when it's released , The rest of the context cannot be returned to the queue , Release directly , At the same time, the context instance pool will end and no longer have the ability to maintain and process the context . Let's go back to leasing again , When there is a context available in the queue , You can see that each time you re instantiate a context and the context instance pool management context, the essential difference between context management and Resurrect Method handling .
((IDbContextPoolable)context).Resurrect(_configurationSnapshot);
Let's take a look at how this method works , Is there any magic that affects performance , We must use instance pool in the specified scenario ?
void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot) { if (configurationSnapshot.AutoDetectChangesEnabled != null) { ChangeTracker.AutoDetectChangesEnabled = configurationSnapshot.AutoDetectChangesEnabled.Value; ChangeTracker.QueryTrackingBehavior = configurationSnapshot.QueryTrackingBehavior.Value; ChangeTracker.LazyLoadingEnabled = configurationSnapshot.LazyLoadingEnabled.Value; ChangeTracker.CascadeDeleteTiming = configurationSnapshot.CascadeDeleteTiming.Value; ChangeTracker.DeleteOrphansTiming = configurationSnapshot.DeleteOrphansTiming.Value; } else { ((IResettableService)_changeTracker)?.ResetState(); } if (_database != null) { _database.AutoTransactionsEnabled = configurationSnapshot.AutoTransactionsEnabled == null || configurationSnapshot.AutoTransactionsEnabled.Value; } }
wow , We were stunned , Nothing at all , We don't have to explain , It's just a simple set of change tracking state properties . without doubt , Context instances do reuse context instances , If there are complex business logic and high throughput , Using the context instance pool obviously outperforms the context , besides , In essence, there is no big performance difference between them . Because based on our analysis above , If you use context directly , Every time you build a context instance , It doesn't take much time , meanwhile , After context instance pool reuses context , It just activates the change tracking properties , It doesn't take much time .
Here we can also see , Context instance pool and thread pool are very different , Thread pool reuses threads , But you can imagine the overhead of creating threads , At the same time, the mechanism of thread reuse is completely different , as far as I am concerned , The thread pool has multiple queues , For N Threads , Yes N+1 A queue , Each thread has a local queue and a global queue , As for selecting which thread task to enter which queue, see the corresponding rules .
summary
Analysis so far , Let's make a complete comparative analysis of the injection context and the context instance pool . The default context period is Scope And can be customized , The context period managed by the context instance pool is Scope, Can't change , The default size of the context instance pool is 128, We can also rewrite the corresponding method , If not given maxSize( Can be empty ), The default pool size is 32. If there is a rentable context in the context instance pool queue , Take out , Then just activate the change tracking response properties , Otherwise, create the context instance directly . If the return context exceeds the context instance pool queue size ( Custom pool size ), Release the remaining context directly , Of course, it is no longer managed by the context instance pool .