项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍然适用。

先说结论,可以转到ServiceProvider章节,为了在性能与开销中获取平衡,Microsoft.Extensions.DependencyInjection在初次请求时使用反射实例化目标服务,再次请求时异步使用表达式树替换了目标实例化委托,使得后续请求将得到性能提升。

IServiceProviderEngine

依赖注入的核心是IServiceProviderEngine,它定义了GetService()方法,再被IServiceProvider间接调用。

IServiceProviderEngine包含若干实现,由ServiceProvider的构造函数的参数决定具体的实现类型。由于ServiceProviderOptions.Mode是内部可见枚举,默认值为ServiceProviderMode.DynamicServiceCollectionContainerBuilderExtensions.BuildServiceProvider()作为入口没有控制能力,使得成员_engine是类型为DynamicServiceProviderEngine的实例。

最终实现类DynamicServiceProviderEngineCompiledServiceProviderEngine继承,后者再从抽象类ServiceProviderEngine继承。

抽象类ServiceProviderEngine定义了方法GetService(Type serviceType),并维护了默认可见性的线程安全的字典internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices,目标类型实例化总是先从该字典获取委托。

sequenceDiagram
User-->>IServiceCollection: AddTransient<TService>
User->>+IServiceCollection: BuildServiceProvider()
IServiceCollection->>-User: ServiceProvider
ServiceProvider-->>DynamicServiceProviderEngine: new
DynamicServiceProviderEngine-->>CompiledServiceProviderEngine: base
CompiledServiceProviderEngine-->>ServiceProviderEngine: base
User->>+ServiceProvider: GetService<TService>()
ServiceProvider->>+ServiceProviderEngine: GetService()
ServiceProviderEngine->>ServiceProviderEngine: this.RealizedServices.GetOrAdd()
note right of ServiceProviderEngine: internal
ServiceProviderEngine->>-ServiceProvider: TService
ServiceProvider->>-User: TService

方法ServiceProviderEngine.GetService()并不是抽象方法,上述两个个实现类也没有重写。方法被调用时,ServiceProviderEngine的私有方法CreateServiceAccessor(Type serviceType)首先使用CallSiteFactory分析获取待解析类型的上下文IServiceCallSite,接着调用子类的RealizeService(IServiceCallSite)实现。

ServiceProviderEngine

这里解析两个重要依赖CallSiteFactoryCallSiteRuntimeResolver,以及数据结构IServiceCallSite,前两者在ServiceProviderEngine的构造函数中得到实例化。

CallSiteFactory

ServiceProviderEngine以注入方式集合作为构建函数的参数,但参数被立即转交给了CallSiteFactory,后者在维护注入方式集合与了若干字典对象。

  • List<ServiceDescriptor> _descriptors:所有的注入方式集合
  • Dictionary<Type, IServiceCallSite> _callSiteCache:目标服务类型与其实现的上下文字典
  • Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup:使用目标服务类型分组后注入方式映射

ServiceDescriptorCacheItem是维护了List<ServiceDescriptor>的结构体,CallSiteFactory总是使用最后一个注入方式作为目标类型的实例化依据。

IServiceCallSite

IServiceCallSite是目标服务类型实例化的上下文,CallSiteFactory通过方法CreateCallSite()创建IServiceCallSite,并通过字典_callSiteCache进行缓存。

  • 首先尝试调用针对普通类型的TryCreateExact()方法;
  • 如果前一步为空,接着尝试调用针对泛型类型的TryCreateOpenGeneric()方法;
  • 如果前一步为空,继续深度调用针对可枚举集合的 TryCreateEnumerable()方法;
  • TryCreateEnumerable()内部使用了TryCreateExact()TryCreateOpenGeneric()

CallSiteFactory对不同注入方式有选取优先级,优先选取实例注入方式,其次选取委托注入方式,最后选取类型注入方式,以 TryCreateExact()为例简单说明:

  1. 对于使用单例和常量的注入方式,返回ConstantCallSite实例;
  2. 对于使用委托的注入方式,返回FactoryCallSite实例;
  3. 对于使用类型注入的,CallSiteFactory调用方法CreateConstructorCallSite()
    • 如果只有1个构造函数

      • 无参构造函数,使用 CreateInstanceCallSite作为实例化上下文;
      • 有参构造函数存,首先使用方法CreateArgumentCallSites()遍历所有参数,递归创建各个参数的 IServiceCallSite 实例,得到数组。接着使用前一步得到的数组作为参数, 创建出 ConstructorCallSite实例。
    • 如果多于1个构造函数,检查和选取最佳构造函数再使用前一步逻辑处理;
  4. 最后添加生命周期标识

泛型、集合处理多了部分前置工作,在此略过。

如下流程图简要地展示了递归过程:

CallSiteRuntimeResolver

CallSiteRuntimeResolverCallSiteVisitor<ServiceProviderEngineScope, object>继承,被抽象类ServiceProviderEngine依赖,被DynamicServiceProviderEngine间接引用。

由于目标服务类型实例化上下文已经由CallSiteFactory获取完成,该类的工作集中于类型推断与调用合适的方法实例化取目标服务。

  • ConstantCallSite:获取引用的常量;
  • FactoryCallSite:执行委托;
  • CreateInstanceCallSite:反射调用Activator.CreateInstance()
  • ConstructorCallSite:递归实例化各个参数得到数组,接着作为参数反射调用ConstructorInfo.Invoke()

前面提到ServiceProviderEngine维护了字典,用于该委托的存取,后面继续会讲到。

ServiceProviderEngine.GetService()内部使用其私有方法CreateServiceAccessor(),传递CallSiteFactory获取到IServiceCallSite实例到子类重写的方法RealizeService(),故关注点回到DynamicServiceProviderEngine

DynamicServiceProviderEngine

DynamicServiceProviderEngine重写父类方法RealizeService(),返回了一个特殊的委托,委托内包含了对父类CompiledServiceProviderEngine和抽象类ServiceProviderEngine的成员变量的调用。

  • 该委托被存储到ServiceProviderEngine维护的字典;
  • 该委托被第1次调用时,使用ServiceProviderEngine内部类型为CallSiteRuntimeResolver的成员完成目标服务的实例化;
  • 该委托被第2次调用时,除了第1步外,额外另起 Task 调用父类CompiledServiceProviderEngine内部类型为ExpressionResolverBuilder成员的方法Resolve()得到委托,替换前述的ServiceProviderEngine维护的字典内容。

委托的前2次执行结果总是由ServiceProviderEngine.RuntimeResolver返回的。

sequenceDiagram
ServiceProvider->>+ServiceProviderEngine: GetService()
ServiceProviderEngine->>ServiceProviderEngine: RealizedServices.GetOrAdd()
ServiceProviderEngine->>ServiceProviderEngine: CreateServiceAccessor(Type serviceType)
%% Func<ServiceProviderEngineScope, object>
ServiceProviderEngine->>+CallSiteFactory: CreateCallSite(Type serviceType)
CallSiteFactory->>-ServiceProviderEngine: IServiceCallSite
note left of CallSiteFactory: context of TService
ServiceProviderEngine->>+DynamicServiceProviderEngine: RealizeService(IServiceCallSite)
alt Interlocked.Increment(ref callCount) == 2
DynamicServiceProviderEngine-->>CompiledServiceProviderEngine: Task.Run(() => base.RealizeService())
end
DynamicServiceProviderEngine->>+CompiledServiceProviderEngine: base.RuntimeResolver.Resolve()
CompiledServiceProviderEngine->>-DynamicServiceProviderEngine: Func<ServiceProviderEngineScope, object>
DynamicServiceProviderEngine->>-ServiceProviderEngine: delegate
ServiceProviderEngine->>ServiceProviderEngine: execute delegate with scope
ServiceProviderEngine->>-ServiceProvider: TService

CompiledServiceProviderEngine

CompiledServiceProviderEngine依赖了ExpressionResolverBuilder,并操作了抽象类ServiceProviderEngine维护的字典对象RealizedServices

ExpressionResolverBuilder

ExpressionResolverBuilderCallSiteVisitor<CallSiteExpressionBuilderContext, Expression>继承,正如其名是表达式树的相关实现,其方法Build()构建和返回类型为Func<ServiceProviderEngineScope, object>的委托。

ExpressionResolverBuilderCallSiteRuntimeResolver一样继承了抽象类CallSiteVisitor<TArgument, TResult>,所以解析出表达式树的过程极其相似,根据 IServiceCallSite创建出表达式树。

  • ConstantCallSite:使用Expression.Constant()
  • FactoryCallSite:使用Expression.Invocation()
  • CreateInstanceCallSite:使用 Expression.New()
  • ConstructorCallSite:递归创建各个参数的表达式树得到数组,接着作为参数,使用Expression.New() 创建最终的表达式树;

ServiceProvider

回顾整个流程可知,CallSiteFactoryCallSiteRuntimeResolverExpressionResolverBuilder是目标服务实例化的核心实现:

  • CallSiteFactory:解析和缓存目标服务的实例化上下文;
  • CallSiteRuntimeResolver:使用反射完成目标服务的实例化;
  • ExpressionResolverBuilder:使用表达式树得到目标服务的实例化的前置委托;

ServiceProvider通过特殊的委托完成了目标服务实例化方式的替换:

  • 初次调用GetService()

    • 首先通过DynamicServiceProviderEngine返回了委托,该委托被存储到字典 RealizedServices中;
    • 接着该委托被第1次执行,通过CallSiteRuntimeResolver完成目标服务的实例化;
  • 再将调用GetService()时,
    • 直接得到缓存的委托并同样完成目标服务的实例
    • 同时通过一个额外的 Task,通过ExpressionResolverBuilder 使用表达式树重新生成委托,并操作字典RealizedServices,替换初次调用生成委托;
  • 后续调用GetService()时,字典RealizedServices查找到的是已经替换过的使用表达式树生成的委托。

没有线程安全问题,委托一定会被替换,视表达式树的构建完成时机。

sequenceDiagram
ServiceProviderEngine->>+DynamicServiceProviderEngine: RealizeService(IServiceCallSite)
alt Interlocked.Increment(ref callCount) == 2
DynamicServiceProviderEngine->>+CompiledServiceProviderEngine: Task.Run(() => base.RealizeService(IServiceCallSite))
CompiledServiceProviderEngine->>+ExpressionResolverBuilder: Build(IServiceCallSite)
note right of ExpressionResolverBuilder: ExpressionTree
ExpressionResolverBuilder->>-CompiledServiceProviderEngine: func: Func<ServiceProviderEngineScope, object>
CompiledServiceProviderEngine-->>ServiceProviderEngine: base.RealizedServices[callSite.ServiceType] = func
note right of ServiceProviderEngine: Rewrite cache
end

DynamicServiceProviderEngine->>+ServiceProviderEngine: base.RuntimeResolver.Resolve(IServiceCallSite)
ServiceProviderEngine->>+CallSiteRuntimeResolver: Resole(IServiceCallSite, scope)
note right of CallSiteRuntimeResolver: Reflection
CallSiteRuntimeResolver->>-ServiceProviderEngine: Func<ServiceProviderEngineScope, object>
ServiceProviderEngine->>-DynamicServiceProviderEngine: delegate
DynamicServiceProviderEngine->>-ServiceProviderEngine: delegate
ServiceProviderEngine->>ServiceProviderEngine: execute delegate with scope

Summary

Microsoft.Extensions.DependencyInjection 2.x 希望在开销和性能中取得平衡,其实现方式是使用特殊委托完成委托本身的替换。``CallSiteVisitor` 是获取实例和表示式树的核心实现。

由于表达式创建的过程中不存在对参数的表达式树的缓存过程,故对于 A 依赖 B 的情况,如果只是获取 A ,使得 A 的表达式树构建完成并以委托形式缓存,单独获取 B 仍然要完成先反射后构造表达式的流程,见 CompiledServiceProviderEngine

掌握了 Microsoft.Extensions.DependencyInjection 2.x 的实现机制,加上对内存 dump 的对比,已经知道表达式树的构建过程是产生开销的原因,出于篇幅控制另行展开。

leoninew 原创,转载请保留出处 www.cnblogs.com/leoninew

解析 Microsoft.Extensions.DependencyInjection 2.x 版本实现的更多相关文章

  1. 使用诊断工具观察 Microsoft.Extensions.DependencyInjection 2.x 版本的内存占用

    目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...

  2. Microsoft.Extensions.DependencyInjection 之一:解析实现

    [TOC] 前言 项目使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次请求时非常高的内存占用情况,于是作了调查,本文对 3.0 版本仍 ...

  3. Microsoft.Extensions.DependencyInjection 之三:展开测试

    目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...

  4. Microsoft.Extensions.DependencyInjection 之三:反射可以一战(附源代码)

    目录 前文回顾 IServiceCallSite CallSiteFactory ServiceProviderEngine CompiledServiceProviderEngine Dynamic ...

  5. Microsoft.Extensions.DependencyInjection 之二:使用诊断工具观察内存占用

    目录 准备工作 大量接口与实现类的生成 elasticsearch+kibana+apm asp.net core 应用 请求与快照 Kibana 上的请求记录 请求耗时的分析 请求内存的分析 第2次 ...

  6. DotNetCore跨平台~一起聊聊Microsoft.Extensions.DependencyInjection

    写这篇文章的心情:激动 Microsoft.Extensions.DependencyInjection在github上同样是开源的,它在dotnetcore里被广泛的使用,比起之前的autofac, ...

  7. 使用 Microsoft.Extensions.DependencyInjection 进行依赖注入

    没有 Autofac DryIoc Grace LightInject Lamar Stashbox Unity Ninject 的日子,才是好日子~~~~~~~~~~ Using .NET Core ...

  8. MvvmLight + Microsoft.Extensions.DependencyInjection + WpfApp(.NetCore3.1)

    git clone MvvmLight失败,破网络, 就没有直接修改源码的方式来使用了 Nuget安装MvvmLightLibsStd10 使用GalaSoft.MvvmLight.Command命名 ...

  9. Microsoft.Extensions.DependencyInjection中的Transient依赖注入关系,使用不当会造成内存泄漏

    Microsoft.Extensions.DependencyInjection中(下面简称DI)的Transient依赖注入关系,表示每次DI获取一个全新的注入对象.但是使用Transient依赖注 ...

随机推荐

  1. 第八届蓝桥杯java b组第十题

    标题: k倍区间 给定一个长度为N的数列,A1, A2, ... AN,如果其中一段连续的子序列Ai, Ai+1, ... Aj(i <= j)之和是K的倍数,我们就称这个区间[i, j]是K倍 ...

  2. 敏捷测试--之scrum--原理

    Scrum 是一个用于开发和维持复杂产品的框架 ,是一个增量的.迭代的开发过程.在这个框架中,整个开发过程由若干个短的迭代周期组成,一个短的迭代周期称为一个Sprint,每个Sprint的建议长度是2 ...

  3. electron教程(三): 使用ffi-napi引入C++的dll

    我的electron教程系列 electron教程(一): electron的安装和项目的创建 electron教程(二): http服务器, ws服务器, 进程管理 electron教程(三): 使 ...

  4. 免费下载 80多种的微软推出入门级 .NET视频

    .NET Core 3.0发布视频系列中宣布了80多个新的免费视频,这些视频同时放在Microsoft的Channel 9 和youtube上面. 在线观看由于跨洋网络效果不太好,下载到机器上慢慢上是 ...

  5. 使用gtest(googletest)进行c++单元测试

    这是系列文章的第三篇,前两篇https://www.cnblogs.com/gaopang/p/11243367.html和https://www.cnblogs.com/gaopang/p/1158 ...

  6. Java 学习笔记之 Daemon线程

    Daemon线程: 线程: 用户线程 守护线程 守护线程是一种特殊的线程,在进程中不存在非守护线程了,则守护线程自动销毁. public class DaemonThread extends Thre ...

  7. echarts地图边界数据的实时获取与应用,省市区县多级联动【附最新geoJson文件下载】

    首先,来看下效果图 在线体验地址:https://hxkj.vip/demo/echartsMap/,并提供实时geoJson数据文件下载 echarts官方社区链接地址(可在线编辑):https:/ ...

  8. 程序员修神之路--设计一套RPC框架并非易事

    菜菜哥,我最近终于把Socket通信调通了 这么底层的东西你现在都会了,恭喜你离涨薪又进一步呀 http协议不也是利用的Socket吗 可以这么说,http协议是基于TCP协议的,底层的数据传输可以说 ...

  9. Java 添加、修改、读取、删除PPT备注

    概述 幻灯片中的备注信息是只提供给幻灯片演讲者观看的特定内容,在演讲者放映幻灯片时,备注信息可给演讲者提供讲解思路,起到辅助讲解的作用.本文将通过Java程序来演示如何操作PPT幻灯片中的备注信息,要 ...

  10. Kubernetes的RBAC是啥

    RBAC: Role-Based Access Control,基于角色的权限控制,有以下三种角色 Role:角色,它其实是一组规则,定义了一组API对象的操作权限 Subject:被作用者,可以是人 ...