当前位置:网站首页>Case analysis of entitycore framework

Case analysis of entitycore framework

2020-11-08 18:41:00 Jeff.

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 .

版权声明
本文为[Jeff.]所创,转载请带上原文链接,感谢