title: AspNetCore底层源码剖析(三)IOC
date: 2022-09-21 13:20:01
categories: 后端
tags:
- .NET

介绍

每个 ASP.NET Core 应用程序都有一个根级别的IServiceProvider,除了Root级别的IServiceProvider之外,IServiceProvider还可以创建多个新的ScopeIServiceScope),Scope内有自己的IServiceProvider,当Scope被释放时,它也会释放其中所有的ScopeTransient级别的对象。

关于服务的生命周期范围一共就三种:

  1. Singleton 跟应用的生命周期一致
  2. Scoped 跟容器的生命周期一致
  3. Transient 每次获取都创建新的对象

在 ASP.NET Core 中会为每个请求创建一个新范围。这意味着给定请求的所有 Scoped 服务都是从同一个容器中解析的,因此对于任意一个请求,在任何地方都使用相同的 Scoped 服务实例。在请求结束时,Scope本身和所有已解析的服务一起被释放。每个请求都有一个新的范围,因此 Scoped 服务彼此隔离。

重点:如果有一个服务不是从http请求处理的过程中解析出来的,比如自己加了一个BackgroundService在后台运行,并解析了一些Scoped和Transient级别的服务,那这些服务还会被释放吗?答案是不会,下面我们来看下源码,到底发生了什么

分析源码

HTTP请求

注意,下面给出的源码中省略了大部分与IOC无关的代码逻辑

当一个http请求进来的时候,首先是调用如下方法,开始处理这个请求:

 private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application) where TContext : notnull
{ BeginRequestProcessing();
// 重点
var context = application.CreateContext(this); await application.ProcessRequestAsync(context); }

CreateContext函数用于初始化HttpContext对象,下面是这个函数的完整代码:

public Context CreateContext(IFeatureCollection contextFeatures)
{
Context? hostContext;
if (contextFeatures is IHostContextContainer<Context> container)
{
hostContext = container.HostContext;
if (hostContext is null)
{
hostContext = new Context();
container.HostContext = hostContext;
}
}
else
{
// Server doesn't support pooling, so create a new Context
hostContext = new Context();
} HttpContext httpContext;
if (_defaultHttpContextFactory != null)
{
var defaultHttpContext = (DefaultHttpContext?)hostContext.HttpContext;
if (defaultHttpContext is null)
{
httpContext = _defaultHttpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
else
{
_defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);
httpContext = defaultHttpContext;
}
}
else
{
httpContext = _httpContextFactory!.Create(contextFeatures);
hostContext.HttpContext = httpContext;
} _diagnostics.BeginRequest(httpContext, hostContext);
return hostContext;
}

这里面最重要的就是_defaultHttpContextFactory这个类,一看名字就知道是个HttpContext的工厂类,其创建代码如下:

public HttpContext Create(IFeatureCollection featureCollection)
{
if (featureCollection is null)
{
throw new ArgumentNullException(nameof(featureCollection));
} var httpContext = new DefaultHttpContext(featureCollection);
Initialize(httpContext);
return httpContext;
}

在内部它每次都会根据传进来的featureCollection初始化一个DefaultHttpContext,而这个DefaultHttpContext内部最重要的属性是下面这个:


// 核心
public override IServiceProvider RequestServices
{
get { return ServiceProvidersFeature.RequestServices; }
set { ServiceProvidersFeature.RequestServices = value; }
} private IServiceProvidersFeature ServiceProvidersFeature =>
_features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!;

调试时发现_features的更新和清除比较复杂,就没有细研究,不过不影响最终结论

下面是Debug过程中初始化之后这个属性的值:

可以看到并不是根范围的IServiceProvider,当HttpContext初始化完毕后,接下来再根据管道往下调用:

 public Task ProcessRequestAsync(Context context)
{
return _application(context.HttpContext!);
}

当单步调试后,我们会进入一个很重要的管道:

public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args)
{
// 上面省略大量代码
var factory = Compile<object>(methodInfo, parameters); return context =>
{
// 核心
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
} return factory(instance, context, serviceProvider);
};
});
}

在这里,var serviceProvider = context.RequestServices ?? applicationServices;这句话决定当前这个处理的请求是使用了Scope级别的IServiceProvider还是Root级别的IServiceProvider,当然,一般情况下context.RequestServices这个值都是存在的,所以我们每个请求的内的IServiceProvider都是在子范围内,最后当请求结束时这个IServiceProvider会销毁,里面解析的Scoped和Transient级别的服务自然也就销毁了

后台服务

当我们在不是http请求进来的其他服务中使用IServiceProvider时,它其实是一个根级别的,下面来写个例子证实一下:


public class TestService : BackgroundService
{
private readonly ILogger<TestService> _log;
private readonly IServiceProvider _provider;
private readonly IServiceScope _scope; public TestService(ILogger<TestService> logger,IServiceProvider provider)
{
_log = logger;
_provider = provider;
_scope=_provider.CreateScope(); }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{ //执行任务
Console.WriteLine($"{DateTime.Now}");
var t2=_provider.GetService<Test>();
t2.Tag = "Root";
// var t3=_scope.ServiceProvider.GetService<Test>();
t3.Tag = "Root";
//周期性任务,于上次任务执行完成后,等待50毫秒,执行下一次任务
await Task.Delay(50);
} } public override void Dispose()
{
Console.WriteLine("Disposed");
base.Dispose();
} public class Test:IDisposable
{
public string Tag { get; set; } = "Default";
public List<long> data = new List<long>(10000); public Test()
{
for (int i = 0; i < 10000; i++)
{
data.Add(i);
}
}
~Test()
{
Console.WriteLine($"DisConstructor Test {this.GetHashCode().ToString() } {Tag}"); }
public void Dispose()
{
Console.WriteLine($"Disposed Test {this.GetHashCode().ToString() } {Tag}");
}
} public class RootTest:IDisposable
{
public string Tag { get; set; } = "Default"; ~RootTest()
{
Console.WriteLine($"DisConstructor Root Test {this.GetHashCode().ToString() } {Tag}"); }
public void Dispose()
{
Console.WriteLine($"Disposed Root Test {this.GetHashCode().ToString() } {Tag}");
}
}
}

接下来注入这三个服务:

builder.Services.AddTransient(typeof(TestService.Test)); // 瞬时级别
builder.Services.AddHostedService<TestService>(); // 后台服务
builder.Services.AddSingleton<TestService.RootTest>(); // 全局级别

我们需要关注的是var t2=_provider.GetService<Test>();这一行,我们启动程序后单步调试,可以看到进入了下面这段代码:

public bool IsRootScope { get; }

internal ServiceProvider RootProvider { get; }

public object GetService(Type serviceType)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
} return RootProvider.GetService(serviceType, this);
}

很显然,从名字上都可以知道,这个RootProvider其实是一个根级别的IServiceProvider

内存泄漏

当我们在根容器内解析Scoped或者Transient级别的服务时,就会出现内存泄漏,因为除非根容器销毁(等同于程序退出),否则所有内部解析出来的服务都不会被销毁,还是以上一小节的Test类为例子,内部每次都会创建一个包含10000个long类型变量的数组,如果我们像下面这样解析服务:

while (!stoppingToken.IsCancellationRequested)
{ //执行任务
Console.WriteLine($"{DateTime.Now}");
var t3=_scope.ServiceProvider.GetService<Test>();
t3.Tag = "Root";
//周期性任务,于上次任务执行完成后,等待50毫秒,执行下一次任务
await Task.Delay(50);
}

Test在注册的时候用的是AddTransient,同时_scope并没有调用自己的Dispose,那么上面每50毫秒都会解析出一个新Test实例,并且不会被销毁!下面这段运行时的内存分析截图可以证明结论:

同时Test类的析构函数和Dispose函数都没有被调用,这也可以证明结论是正确的

不仅Transient级别是这样的,Scoped级别同理也不会被释放,Singleton除外,Scoped级别的容器被销毁Singleton也不会被销毁,除非根容器销毁

最后总结

  1. 每个http请求都会创建一个IServiceScope,内部有一个自己的IServiceProvider,包括在控制器中注入的也是,可以通过HttpContext.RequestService获取
  2. 如果不是从Http请求进来的,比如后台服务,那么获取到的是根级别的IServiceProvider,我们需要自己创建一个范围容器来解析服务
  3. Singleton级别的服务如果需要Dipose,需要自己手动调用Dispose方法,否则不会被释放(socket连接,文件句柄等),或者使用委托方法注册,这样就由IOC容器来管理了,建议用第二种方式

如果需要用Rider调试,要在设置中勾上下面两项:

否则会出现这个问题:Evaluation is not allowed: The thread is not at a GC-safe point site:stackoverflow.com,导致看不到调试过程中一些中间变量的值

参考

  1. https://github.com/dotnet/aspnetcore/issues/31478
  2. https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-in-asp-net-core/
  3. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2#scope-validation
  4. https://www.cnblogs.com/wucy/p/13268296.html
  5. https://github.com/dotnet/aspnetcore/issues/2826
  6. https://youtrack.jetbrains.com/issue/RIDER-45516/Cannot-evaluate-debug-assertion-in-net-core-31
  7. https://stackoverflow.com/questions/56032041/how-can-i-access-iservicecollection-from-a-background-thread

AspNetCore底层源码剖析(三)IOC的更多相关文章

  1. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析——核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring源码ioc编程bean 更多 个人分类: Java https://blog ...

  2. jdk源码剖析三:锁Synchronized

    一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...

  3. Django Rest Framework源码剖析(三)-----频率控制

    一.简介 承接上篇文章Django Rest Framework源码剖析(二)-----权限,当服务的接口被频繁调用,导致资源紧张怎么办呢?当然或许有很多解决办法,比如:负载均衡.提高服务器配置.通过 ...

  4. 2018.11.20 Struts2中对结果处理方式分析&struts2内置的方式底层源码剖析

    介绍一下struts2内置帮我们封装好的处理结果方式也就是底层源码分析 这是我们的jar包里面找的位置目录 打开往下拉看到result-type节点 name那一列就是我们的type类型取值 上一篇博 ...

  5. python部分重点底层源码剖析

    Python源码剖析—Set容器(hashtable实现) python源码剖析(内存管理和垃圾回收)

  6. (文字版)Qt信号槽源码剖析(三)

    大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的Qt宏展开推导:今天接着深入分析,进入Qt信号槽源码剖析系列的第三节视频. Qt信号槽宏推导归纳 #define si ...

  7. Dubbo源码剖析三之服务注册过程分析

    Dubbo源码剖析二之注册中心 - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中对注册中心进行了简单的介绍,对Dubbo整合Zookeeper链接源码进行了详细分析.本文接着对服务注册过 ...

  8. Springboot拦截器使用及其底层源码剖析

    博主最近看了一下公司刚刚开发的微服务,准备入手从基本的过滤器以及拦截器开始剖析,以及在帮同学们分析一下上次的jetty过滤器源码与本次Springboot中tomcat中过滤器的区别.正题开始,拦截器 ...

  9. boost.asio源码剖析(三) ---- 流程分析

    * 常见流程分析之一(Tcp异步连接) 我们用一个简单的demo分析Tcp异步连接的流程: #include <iostream> #include <boost/asio.hpp& ...

  10. HashMap底层源码剖析

    HashMap底层源码剖析 一.HashMap底层用到的数据结构 数组+单向链表+红黑树 数组:数组每一项都是一个链表,其实就是数组和链表的结合体 单向链表:当法神hash碰撞时,首先会找到数组对应位 ...

随机推荐

  1. HTML+CSS基础知识(4)简单的广告界面

    文章目录 1.网页实例 1.1 代码 1.2 测试效果 1.网页实例 1.1 代码 css样式 /* 清除页面样式 */ *{ margin:0; padding: 0; } /* 统一页面的样式 * ...

  2. 获取cpu的核数

    //获取cpu的核数 System.out.println(Runtime.getRuntime().availableProcessors());

  3. 二、docker安装

    一.docker安装 Docker 是管理容器的工具, Docker 不等于 容器. 1.1.docker yum源设置 #step 1 download docker-ce.repo file [r ...

  4. UML建模语言、设计原则、设计模式

    1.UML统一建模语言 定义:用于软件系统设计与分析的语言工具 目的:帮助开发人员更好的梳理逻辑.思路 学习地址:UML概述_w3cschool 官网:https://www.omg.org/spec ...

  5. 系统整理K8S的配置管理实战-建议收藏系列

    目录 一.ConfigMap 1.1.创建 1.1.1.from-file 1.1.2.from-env-file 1.1.3.from-literal 1.1.4.基于yaml文件创建 1.2.Po ...

  6. c++ 关于引用变量你不知道的东西

    引用变量延迟绑定 我们知道引用变量定义时要立刻赋值,告诉编译器他是谁的引用.如果不赋值,编译会失败. 如果引用变量是单个定义的,对他赋值还比较简单. struct test_T { int data; ...

  7. 【翻译】Spring Security - 如何解决WebSecurityConfigurerAdapter类已被弃用的问题?

    原文链接:Spring Security - How to Fix WebSecurityConfigurerAdapter Deprecated 原文作者:Nam Ha Minh 原文发表日期:20 ...

  8. K8s 生产最佳实践-限制 NameSpace 资源用量

    前言 想象一下这个场景:多个系统运行在同一套 K8s 集群上,有重要系统,也有不太重要的系统.但是某一天,某个不重要的系统突然占用了该 K8s 集群的所有资源,导致该集群上的其他系统的正常运行受到影响 ...

  9. 关于CSDN微信登录接口的研究

    代码 import requests import re from threading import Thread import time import requests from io import ...

  10. ArcObjects SDK开发 001 ArcObjects SDK 简介

    1.什么是ArcObjects SDK 在网上搜索什么是ArcObjects,会搜到如下的定义. 这个定义比较准确,也比较容易理解. 2.什么是ArcEngine 在网上搜索ArcEngine,一般会 ...