網(wǎng)站首頁 編程語言 正文
前言
問題的起因是在幫同事解決遇到的一個問題,他的本意是在EF Core中為了解決避免多個線程使用同一個DbContext
實例的問題。但是由于對Microsoft.Extensions.DependencyInjection
體系的深度不是很了解,結(jié)果遇到了新的問題,當(dāng)時整得我也有點蒙了,所以當(dāng)時也沒解決,而且當(dāng)時快下班了,就想著第二天再解決。在地鐵上,經(jīng)過我一系列的思維跳躍,終于想到了問題的原因,第二天也順利的解決了這個問題。雖然我前面說了EFCore,但是本質(zhì)和EFCore沒有關(guān)系,只是湊巧。解決了之后覺得這個問題是個易錯題,覺得挺有意思的,便趁機記錄一下。
問題演示
接下來我們還原一下當(dāng)時的場景,以下代碼只是作為演示,無任何具體含義,只是為了讓操作顯得更清晰一下,接下來就貼一下當(dāng)時的場景代碼
[Route("api/[controller]/[action]")] [ApiController] public class InformationController : ControllerBase { private readonly LibraryContext _libraryContext; private readonly IServiceProvider _serviceProvider; private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext, IServiceProvider serviceProvider, ILogger<InformationController> logger) { _libraryContext = libraryContext; _serviceProvider = serviceProvider; _logger = logger; } [HttpGet] public string GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //這里直接使用了Task方式 Task.Run(() => { try { //Task里創(chuàng)建了新的IServiceScope using var scope = _serviceProvider.CreateScope(); //通過IServiceScope創(chuàng)建具體實例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; } }
再次強調(diào)一下,上述代碼純粹是為了讓演示更清晰,無任何業(yè)務(wù)含義,不喜勿噴。咱們首先看一下這段代碼表現(xiàn)出來的意思,就是在ASP.NET Core
的項目里,在Task.Run
里使用IServiceProvider
去創(chuàng)建Scope的場景。如果對ASP.NET Core Controller生命周期和IServiceProvider不夠了解的話,會很容易遇到這個問題,且不知道是什么原因。上述這段代碼會偶現(xiàn)
一個錯誤
Cannot access a disposed object.
Object name: 'IServiceProvider'.
這里為什么說是偶現(xiàn)呢?因為會不會出現(xiàn)異常完全取決于Task.Run
里的代碼是在當(dāng)前請求輸出之前執(zhí)行完成還是之后完成。說到這里相信有一部分同學(xué)已經(jīng)猜到了代碼報錯的原因了。問題的本質(zhì)很簡單,是因為IServiceProvider
被釋放掉了。我們知道默認情況下ASP.NET Core
為每次請求處理會創(chuàng)建單獨的IServiceScope
,這會關(guān)乎到聲明周期為Scope
對象的聲明周期。所以如果Task.Run
里的邏輯在請求輸出之前執(zhí)行完成,那么代碼運行沒任何問題。如果是在請求完成之后完成再執(zhí)行CreateScope
操作,那必然會報錯。因為Task.Run
里的邏輯何時被執(zhí)行,這個是由系統(tǒng)CPU調(diào)度本身決定的,特別是CPU比較繁忙的時候,這種異常會變得更加頻繁。
這個問題不僅僅是在Task.Run
這種場景里,類似的本質(zhì)就是在一個IServiceScope
里創(chuàng)建一個新的子Scope作用域的時候,這個時候需要注意的是父級的IServiceProvider
釋放問題,如果父級的IServiceProvider
已經(jīng)被釋放了,那么基于這個Provider再去創(chuàng)建Scope則會出現(xiàn)異常。但是這個問題在結(jié)合Task
或者多線程的時候,更容易出現(xiàn)問題。
解決問題
既然我們知道了它為何會出現(xiàn)異常,那么解決起來也就順理成章了。那就是保證當(dāng)前請求執(zhí)行完成之前,最好保證Task.Run
里的邏輯也要執(zhí)行完成,所以我們上述的代碼會變成這樣
[HttpGet] public async Task<string> GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //這里使用了await Task方式 await Task.Run(() => { try { //Task里創(chuàng)建了新的IServiceScope using var scope = _serviceProvider.CreateScope(); //通過IServiceScope創(chuàng)建具體實例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; }
試一下,發(fā)現(xiàn)確實能解決問題,因為等待Task完成能保證Task里的邏輯能在請求執(zhí)行完成之前完成。但是,很多時候我們并不需要等待Task執(zhí)行完成,因為我們就是希望它在后臺線程去執(zhí)行這些操作,而不需要阻塞執(zhí)行。
上面我們提到了本質(zhì)是解決在IServiceScope
創(chuàng)建子Scope時遇到的問題,因為這里注入進來的IServiceProvider
本身是Scope的,只在當(dāng)前請求內(nèi)有效,所以基于IServiceProvider去創(chuàng)建IServiceScope要考慮到當(dāng)前IServiceProvider是否釋放。那么我們就得打破這個枷鎖,我們要想辦法在根容器
中去創(chuàng)建新的IServiceScope。這一點我大微軟自然是考慮到了,在Microsoft.Extensions.DependencyInjection
體系中提供了IServiceScopeFactory
這個根容器的作用域,基于根容器創(chuàng)建的IServiceScope可以得到平行與當(dāng)前請求作用域的獨立的作用域,而不受當(dāng)前請求的影響。改造上面的代碼用以下形式
[Route("api/[controller]/[action]")] [ApiController] public class InformationController : ControllerBase { private readonly LibraryContext _libraryContext; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<InformationController> _logger; public InformationController(LibraryContext libraryContext, IServiceScopeFactory scopeFactory, ILogger<InformationController> logger) { _libraryContext = libraryContext; _scopeFactory = scopeFactory; _logger = logger; } [HttpGet] public string GetFirst() { var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault(); //這里直接使用了Task方式 Task.Run(() => { try { //Task里創(chuàng)建了新的IServiceScope using var scope = _scopeFactory.CreateScope(); //通過IServiceScope創(chuàng)建具體實例 LibraryContext dbContext = scope.ServiceProvider.GetService<LibraryContext>(); var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList(); } catch (Exception ex) { _logger.LogError(ex.Message, ex); } }); return caseInfo.Title; } }
如果你是調(diào)試起來的話你可以看到IServiceScopeFactory的具體實例是Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope
類型的,它里面包含了一個IsRootScope
屬性,通過這個屬性我們可以知道當(dāng)前容器作用域是否是根容器作用域。當(dāng)使用IServiceProvider
實例的時候IsRootScope
為false
,當(dāng)使用IServiceScopeFactory
實例的時候IsRootScope
為true
。使用CreateScope
創(chuàng)建IServiceScope
實例的時候,注意用完了需要釋放,否則可能會導(dǎo)致Transient
和Scope
類型的實例得不到釋放。在之前的文章咱們曾提到過Transient
和Scope
類型的實例都是在當(dāng)前容器作用域釋放的時候釋放的,這個需要注意一下。
問題探究
上面我們了解到了在每次請求的時候使用IServiceProvider
和使用IServiceScopeFactory
的時候他們作用域的實例來源是不一樣的。IServiceScopeFactory
來自根容器,IServiceProvider
則是來自當(dāng)前請求的Scope。順著這個思路我們可以看一下他們兩個究竟是如何的不相同。這個問題還得從構(gòu)建Controller實例的時候,注入到Controller中的實例作用域的問題。
請求中的IServiceProvider
在之前的文章<ASP.NET Core Controller與IOC的羈絆>我們知道,Controller是每次請求都會創(chuàng)建新的實例,我們再次拿出來這段核心的代碼來看一下,在DefaultControllerActivator
類的Create
方法中[點擊查看源碼?]
internal class DefaultControllerActivator : IControllerActivator { private readonly ITypeActivatorCache _typeActivatorCache; public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache) { _typeActivatorCache = typeActivatorCache; } public object Create(ControllerContext controllerContext) { //省略一系列判斷代碼 var serviceProvider = controllerContext.HttpContext.RequestServices; //這里傳遞的IServiceProvider本質(zhì)就是來自HttpContext.RequestServices return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType()); } }
通過這個方法我們可以看到創(chuàng)建Controller實例時,如果存在構(gòu)造依賴,本質(zhì)則是通過HttpContext.RequestServices
實例創(chuàng)建出來的,而它本身就是IServiceProvider
的實例,ITypeActivatorCache
實例中則存在真正創(chuàng)建Controller實例的邏輯,具體可以查看TypeActivatorCache
類的實現(xiàn)[點擊查看源碼?]
internal class TypeActivatorCache : ITypeActivatorCache { private readonly Func<Type, ObjectFactory> _createFactory = (type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes); private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache = new ConcurrentDictionary<Type, ObjectFactory>(); public TInstance CreateInstance<TInstance>( IServiceProvider serviceProvider, Type implementationType) { //省略一系列判斷代碼 var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory); //創(chuàng)建Controller的時候,需要的依賴實例都是來自IServiceProvider return (TInstance)createFactory(serviceProvider, arguments: null); } }
其實在這里我們就可以得到一個結(jié)論,我們在當(dāng)前請求默認通過構(gòu)造注入的IServiceProvider
的實例其實就是HttpContext.RequestServices
,也就是針對當(dāng)前請求的作用域有效,同樣的是來自當(dāng)前作用域的Scope
周期的對象實例也是在當(dāng)前請求結(jié)束就會釋放。驗證這個很簡單可以寫個demo來演示一下
[Route("api/[controller]/[action]")] [ApiController] public class InformationController : ControllerBase { private readonly IServiceProvider _serviceProvider; public InformationController(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [HttpGet] public bool[] JudgeScope([FromServices]IServiceProvider scopeProvider) { //比較構(gòu)造注入的和在HttpContext獲取的 bool isEqualOne = _serviceProvider == HttpContext.RequestServices; //比較通過Action綁定的和在HttpContext獲取的 bool isEqualTwo = scopeProvider == HttpContext.RequestServices; return new[] { isEqualOne, isEqualTwo }; } }
毫無疑問,默認情況
下isEqualOne和isEqualTwo的結(jié)構(gòu)都是true
,這也驗證了我們上面的結(jié)論。因此在當(dāng)前請求默認注入IServiceProvider
實例的時候,都是來自HttpContext.RequestServices
的實例。
請求中的IServiceProvider和IServiceScopeFactory
上面我們看到了在當(dāng)前請求中獲取IServiceProvider
實例本身就是Scope的,而且在當(dāng)前請求中通過各種注入方式獲取到的實例都是相同的。那么接下來我們就可以繼續(xù)跟蹤,本質(zhì)的HttpContext.RequestServices
的IServiceProvider到底來自什么地方呢?我們找到HttpContext默認的實現(xiàn)類DefaultHttpContext
中關(guān)于RequestServices
屬性的定義[點擊查看源碼?]
//接受 public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!; //數(shù)據(jù)來自RequestServicesFeature private static readonly Func<DefaultHttpContext, IServiceProvidersFeature> _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory); //緩存來自_newServiceProvidersFeature private IServiceProvidersFeature ServiceProvidersFeature => _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!; //數(shù)據(jù)來自ServiceProvidersFeature的RequestServices public override IServiceProvider RequestServices { get { return ServiceProvidersFeature.RequestServices; } set { ServiceProvidersFeature.RequestServices = value; } }
通過上面的源碼我們可以看到HttpContext.RequestServices
的數(shù)據(jù)最終來自RequestServicesFeature
類的RequestServices屬性,我們可以直接找到RequestServicesFeature
類的定義[點擊查看源碼?]
public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable { private readonly IServiceScopeFactory? _scopeFactory; private IServiceProvider? _requestServices; private IServiceScope? _scope; private bool _requestServicesSet; private readonly HttpContext _context; public RequestServicesFeature(HttpContext context, IServiceScopeFactory? scopeFactory) { _context = context; _scopeFactory = scopeFactory; } public IServiceProvider RequestServices { get { if (!_requestServicesSet && _scopeFactory != null) { //釋放掉之前沒釋放掉的RequestServicesFeature實例 _context.Response.RegisterForDisposeAsync(this); //通過IServiceScopeFactory創(chuàng)建Scope _scope = _scopeFactory.CreateScope(); //RequestServices來自IServiceScopeFactory的CreateScope實例 _requestServices = _scope.ServiceProvider; //填充已經(jīng)設(shè)置了RequestServices的標(biāo)識 _requestServicesSet = true; } return _requestServices!; } set { _requestServices = value; _requestServicesSet = true; } } //釋放的真實邏輯 public ValueTask DisposeAsync() { switch (_scope) { case IAsyncDisposable asyncDisposable: var vt = asyncDisposable.DisposeAsync(); if (!vt.IsCompletedSuccessfully) { return Awaited(this, vt); } vt.GetAwaiter().GetResult(); break; case IDisposable disposable: disposable.Dispose(); break; } //釋放時重置相關(guān)屬性 _scope = null; _requestServices = null; return default; static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt) { await vt; servicesFeature._scope = null; servicesFeature._requestServices = null; } } //IDisposable的Dispose的方法,通過using可隱式調(diào)用 public void Dispose() { DisposeAsync().AsTask().GetAwaiter().GetResult(); } }
通過上面的兩段源碼,我們得到了許多關(guān)于IServiceProvider和IServiceScopeFactory的相關(guān)信息。
- DefaultHttpContext的RequestServices值來自于RequestServicesFeature實例的RequestServices屬性
- RequestServicesFeature的RequestServices屬性的值通過IServiceScopeFactory通過CreateScope創(chuàng)建的
- 構(gòu)建RequestServicesFeature的IServiceScopeFactory值來自于DefaultHttpContext的ServiceScopeFactory屬性
那么接下來我們直接可以找到DefaultHttpContext的ServiceScopeFactory屬性是誰給它賦的值,我們找到創(chuàng)建HttpContext的地方,在DefaultHttpContextFactory的Create方法里[點擊查看源碼?]
public class DefaultHttpContextFactory : IHttpContextFactory { private readonly IHttpContextAccessor? _httpContextAccessor; private readonly FormOptions _formOptions; private readonly IServiceScopeFactory _serviceScopeFactory; public DefaultHttpContextFactory(IServiceProvider serviceProvider) { _httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>(); _formOptions = serviceProvider.GetRequiredService<IOptions<FormOptions>>().Value; //通過IServiceProvider的GetRequiredService直接獲取IServiceScopeFactory _serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); } //創(chuàng)建HttpContext實例的方法 public HttpContext Create(IFeatureCollection featureCollection) { if (featureCollection is null) { throw new ArgumentNullException(nameof(featureCollection)); } var httpContext = new DefaultHttpContext(featureCollection); Initialize(httpContext); return httpContext; } private DefaultHttpContext Initialize(DefaultHttpContext httpContext) { //IHttpContextAccessor也是在這里賦的值 if (_httpContextAccessor != null) { _httpContextAccessor.HttpContext = httpContext; } httpContext.FormOptions = _formOptions; //DefaultHttpContext的ServiceScopeFactory屬性值來自注入的IServiceProvider httpContext.ServiceScopeFactory = _serviceScopeFactory; return httpContext; } }
這里我們可以看到IServiceScopeFactory的實例來自于通過DefaultHttpContextFactory注入的IServiceProvider實例,這里獲取IServiceScopeFactory的地方并沒有CreateScope,所以這里的IServiceScopeFactory
和IServiceProvider
中的實例都是來自根容器
。這個我們還可以通過注冊DefaultHttpContextFactory
地方看到
[點擊查看源碼?]
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
通過這里可以看到DefaultHttpContextFactory
注冊的是單例模式,注冊它的地方則是在IHostBuilder
的ConfigureServices
方法里。關(guān)于每次請求的創(chuàng)建流程,不是本文的重點,但是為了讓大家對本文講解的IServiceScopeFactory
和IServiceProvider
來源更清楚,咱們可以大致的描述一下
-
GenericWebHostService
類實現(xiàn)自IHostedService
,在StartAsync
方法中啟動了IServer
實例,默認則是啟動的Kestrel。 -
IServer
啟動的方法StartAsync
中會傳遞HostingApplication
實例,構(gòu)建HostingApplication
實例的時候則會依賴IHttpContextFactory
實例,而IHttpContextFactory
實例則是在構(gòu)建GenericWebHostService
服務(wù)的時候注入進來的。 - 當(dāng)每次請求ASP.NET Core服務(wù)的時候會調(diào)用
HostingApplication
的CreateContext
方法,該方法中則會創(chuàng)建HttpContext
實例,每次請求結(jié)束后則調(diào)用該類的DisposeContext
釋放HttpContext
實例。
說了這么多其實就是為了方便讓大家得到一個關(guān)系,即在每次請求中獲取的IServiceProvider
實例來自HttpContext.RequestServices
實例,HttpContext.RequestServices
實例來自IServiceScopeFactory
來自CreateScope
方法創(chuàng)建的實例,而IServiceScopeFactory
實例則是來自根容器,且DefaultHttpContextFactory
的生命周期則和當(dāng)前ASP.NET Core保持一致。
后續(xù)插曲
就在解決這個問題后不久,有一次不經(jīng)意間翻閱微軟的官方文檔,發(fā)現(xiàn)官方文檔有提到相關(guān)的問題,而且也是結(jié)合efcore
來講的。標(biāo)題是《Do not capture services injected into the controllers on background threads》翻譯成中文大概就是不要在后臺線程上捕獲注入控制器的服務(wù),說的正是這個問題,微軟給我們的建議是
- 注入一個IServiceScopeFactory以便在后臺工作項中創(chuàng)建一個范圍。
- IServiceScopeFactory是一個單例對象。
- 在后臺線程中創(chuàng)建一個新的依賴注入范圍。
- 不引用控制器中的任何東西。
- 不從傳入請求中捕獲DbContext。
得到的結(jié)論和我們在本文描述的基本上是差不多的,而且微軟也很貼心的給我們提供了相關(guān)示例
[HttpGet("/fire-and-forget-3")] public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory) { _ = Task.Run(async () => { await Task.Delay(1000); using (var scope = serviceScopeFactory.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>(); context.Contoso.Add(new Contoso()); await context.SaveChangesAsync(); } }); return Accepted(); }
原來還是自己的坑自己最了解,也不得不說微軟現(xiàn)在的文檔確實挺詳細的,同時也提醒我們有空還是得多翻一翻文檔避免踩坑。
總結(jié)
本文主要是通過幫助同事解決問題而得到的靈感,覺得挺有意思的,希望能幫助更多的人了解這個問題,且能避免這個問題。我們應(yīng)該深刻理解ASP.NET Core處理每次請求則都會創(chuàng)建一個Scope
,這會影響當(dāng)前請求獲取的IServiceProvider
實例,和通過IServiceProvider
創(chuàng)建的生命周期為Scope
的實例。如果把握不住,則可以理解為當(dāng)前請求直接注入的服務(wù),和當(dāng)前服務(wù)直接注入的IServiceProvider實例。如果想獲取根容器的實例則可以通過獲取IServiceScopeFactory
實例獲取,最后請注意IServiceScope
的釋放問題。
曾幾何時,特別喜歡去解決遇到的問題,特別喜歡那種解決問題沉浸其中的過程。解決了問題,了解到為什么會讓自己感覺很通透,也更深刻,不經(jīng)意間的也擴展了自己的認知邊界。這個過程得到的經(jīng)驗是一種通識,是一種意識。而思維和意識則是我們適應(yīng)這個不斷在變化時代的底層邏輯。
原文鏈接:https://www.cnblogs.com/wucy/p/16566495.html
相關(guān)推薦
- 2022-12-27 詳解Golang中interface接口的原理和使用技巧_Golang
- 2022-09-12 jQuery事件注冊的實現(xiàn)示范_jquery
- 2023-01-19 Pycharm?2to3配置,python2轉(zhuǎn)python3方式_python
- 2022-09-03 C++中std::conditional的使用說明_C 語言
- 2022-06-01 聊聊.Net,Core配置Nlog.md的問題_實用技巧
- 2022-06-29 python人工智能tensorflow構(gòu)建循環(huán)神經(jīng)網(wǎng)絡(luò)RNN_python
- 2022-11-07 Python根據(jù)字典值對字典進行排序的三種方法實例_python
- 2022-07-11 docker搭建redis 主從哨兵集群
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支