AspNetCore底层源码剖析(三)IOC
title: AspNetCore底层源码剖析(三)IOC
date: 2022-09-21 13:20:01
categories: 后端
tags:
- .NET
介绍
每个 ASP.NET Core 应用程序都有一个根级别的IServiceProvider
,除了Root级别的IServiceProvider
之外,IServiceProvider
还可以创建多个新的Scope
(IServiceScope
),Scope
内有自己的IServiceProvider
,当Scope
被释放时,它也会释放其中所有的Scope
、Transient
级别的对象。
关于服务的生命周期范围一共就三种:
- Singleton 跟应用的生命周期一致
- Scoped 跟容器的生命周期一致
- 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也不会被销毁,除非根容器销毁
最后总结
- 每个http请求都会创建一个
IServiceScope
,内部有一个自己的IServiceProvider
,包括在控制器中注入的也是,可以通过HttpContext.RequestService
获取 - 如果不是从Http请求进来的,比如后台服务,那么获取到的是根级别的
IServiceProvider
,我们需要自己创建一个范围容器来解析服务 - Singleton级别的服务如果需要Dipose,需要自己手动调用
Dispose
方法,否则不会被释放(socket连接,文件句柄等),或者使用委托方法注册,这样就由IOC容器来管理了,建议用第二种方式
如果需要用Rider调试,要在设置中勾上下面两项:
否则会出现这个问题:
Evaluation is not allowed: The thread is not at a GC-safe point site:stackoverflow.com
,导致看不到调试过程中一些中间变量的值
参考
- https://github.com/dotnet/aspnetcore/issues/31478
- https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-in-asp-net-core/
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2#scope-validation
- https://www.cnblogs.com/wucy/p/13268296.html
- https://github.com/dotnet/aspnetcore/issues/2826
- https://youtrack.jetbrains.com/issue/RIDER-45516/Cannot-evaluate-debug-assertion-in-net-core-31
- https://stackoverflow.com/questions/56032041/how-can-i-access-iservicecollection-from-a-background-thread
AspNetCore底层源码剖析(三)IOC的更多相关文章
- 转 Spring源码剖析——核心IOC容器原理
Spring源码剖析——核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring源码ioc编程bean 更多 个人分类: Java https://blog ...
- jdk源码剖析三:锁Synchronized
一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...
- Django Rest Framework源码剖析(三)-----频率控制
一.简介 承接上篇文章Django Rest Framework源码剖析(二)-----权限,当服务的接口被频繁调用,导致资源紧张怎么办呢?当然或许有很多解决办法,比如:负载均衡.提高服务器配置.通过 ...
- 2018.11.20 Struts2中对结果处理方式分析&struts2内置的方式底层源码剖析
介绍一下struts2内置帮我们封装好的处理结果方式也就是底层源码分析 这是我们的jar包里面找的位置目录 打开往下拉看到result-type节点 name那一列就是我们的type类型取值 上一篇博 ...
- python部分重点底层源码剖析
Python源码剖析—Set容器(hashtable实现) python源码剖析(内存管理和垃圾回收)
- (文字版)Qt信号槽源码剖析(三)
大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的Qt宏展开推导:今天接着深入分析,进入Qt信号槽源码剖析系列的第三节视频. Qt信号槽宏推导归纳 #define si ...
- Dubbo源码剖析三之服务注册过程分析
Dubbo源码剖析二之注册中心 - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中对注册中心进行了简单的介绍,对Dubbo整合Zookeeper链接源码进行了详细分析.本文接着对服务注册过 ...
- Springboot拦截器使用及其底层源码剖析
博主最近看了一下公司刚刚开发的微服务,准备入手从基本的过滤器以及拦截器开始剖析,以及在帮同学们分析一下上次的jetty过滤器源码与本次Springboot中tomcat中过滤器的区别.正题开始,拦截器 ...
- boost.asio源码剖析(三) ---- 流程分析
* 常见流程分析之一(Tcp异步连接) 我们用一个简单的demo分析Tcp异步连接的流程: #include <iostream> #include <boost/asio.hpp& ...
- HashMap底层源码剖析
HashMap底层源码剖析 一.HashMap底层用到的数据结构 数组+单向链表+红黑树 数组:数组每一项都是一个链表,其实就是数组和链表的结合体 单向链表:当法神hash碰撞时,首先会找到数组对应位 ...
随机推荐
- echarts的使用 超好用的报表制作、数据的图形化展示
地址链接:https://echarts.apache.org/zh/index.html 1.图形选择 2.对应的js代码
- SQL Server-表结构的操作
1.修改表的字段的数据类型 alter table [File_Info] alter column Upload_Request_ID nvarchar(14) not null 2.添加表的字段并 ...
- RegExp正则表达式的匹配
JavaScript RegExp 对象 RegExp 对象 正则表达式是描述字符模式的对象. 正则表达式用于对字符串模式匹配及检索替换,是对字符串执行模式匹配的强大工具. 语法 var patt=n ...
- webgl(three.js)3D光伏,3D太阳能能源,3D智慧光伏、光伏发电、清洁能源三维可视化解决方案——第十六课
序: 能源是文明和发展的重要保障,人类命运不可避开的话题,无论是战争还是发展,都有它存在的身影.从石器时代到现代文明,人类的能源应用在进步,也在面临能源枯竭的危机与恐惧,而开发与应用可再生能源才是解决 ...
- iptables介绍和基本使用
iptables 防火墙是什么 防火墙好比一堵真的墙,能够隔绝些什么,保护些什么. 防火墙的本义是指古代构筑和使用木制结构房屋的时候,为防止火灾的发生和蔓延,人们将坚固的石块堆砌在房屋周围作为屏障,这 ...
- java将流量KB转换为GB、MB、KB格式
/** * 转换流量格式为xxGBxxMBxxKB * @param flow 156165(xxxxxx) */ public String changeFlowFormat(String flow ...
- Eclipse Python IDE安装
时隔一年,曾经的AI工程师微专业课程也忘了大半,如今终于有闲心重温人工智能的相关知识与项目.先从Eclipse安装开始. 首先下载JDK,进入JDK官网下载最新版本的JDK并安装:https://ww ...
- 读书笔记:A Philosophy of Software Design
今天一位同事在斯坦福的博士生导师John Ousterhout (注,Tcl语言的设计者)来公司做了他的新书<A Philosophy of Software Design>的演讲,介绍了 ...
- css初始化收集
页面元素样式初始化 * { margin: 0; padding: 0; box-sizing: border-box; } html { font-size: 100px; } /* 去掉a链接的文 ...
- NLP实践!文本语法纠错模型实战,搭建你的贴身语法修改小助手 ⛵
作者:韩信子@ShowMeAI 深度学习实战系列:https://www.showmeai.tech/tutorials/42 自然语言处理实战系列:https://www.showmeai.tech ...