解析 Microsoft.Extensions.DependencyInjection 2.x 版本實現

  • 2019 年 11 月 1 日
  • 筆記

項目使用了 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