Microsoft.Extensions.DependencyInjection中(下面簡稱DI)的Transient依賴注入關係,表示每次DI獲取一個全新的注入物件。但是使用Transient依賴注入關係時,最好要配合IServiceScope來一起使用,因為通過Transient依賴注入關係建立的物件,都會被建立它的ServiceProvider物件內部引用,這樣會造成注入物件無法被GC及時回收,造成記憶體洩漏,只有當呼叫ServiceProvider物件的Dispose方法後,ServiceProvider才會解除其內部對注入物件的引用,之後這些注入物件才能被GC回收。
我們新建一個.NET Core控制檯專案,然後假設我們有介面IPeople和實現類People,他們之間的依賴注入關係是Transient。
現在,如果我們的程式碼中有一個for迴圈,它會迴圈1000次,每一次都會從DI中獲取一個IPeople物件例項,由於介面IPeople和類People是Transient關係,所以每次DI都會建立一個新的People物件例項。但是我們只需要在每次迴圈中,呼叫People類的DoSomething方法做一些事情後,就不需要建立的People物件了,也就是說我們希望每次迴圈結束後,GC都能儘量回收在迴圈中建立的People物件例項。
所以我們寫了下面的程式碼:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//註冊介面IPeople和類People的關係為Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //執行1000次迴圈,每一次迴圈建立一個People物件例項,在rootServiceProvider呼叫Dispose方法前,建立的1000個People物件例項都不會被GC回收 for (int i = 0; i < 1000; i++) { IPeople people = rootServiceProvider.GetService<IPeople>(); people.DoSomething(); //在每次迴圈結束後,建立的People物件例項無法被GC回收,因為在rootServiceProvider的內部對所有建立的Transient物件都保持了引用,除非呼叫rootServiceProvider的Dispose方法,否則在每次迴圈中建立的People物件例項都無法被GC回收 } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
從上面程式碼的註釋中,我們可以看到,實際上每一次for迴圈執行完後,GC並不能立即回收在迴圈中建立的People物件例項,原因是ServiceProvider物件rootServiceProvider的內部引用了由DI建立的所有People物件例項,除非呼叫rootServiceProvider的Dispose方法(也就是在上面using程式碼塊最後),否則所有的People物件例項都無法被GC回收。設想一下,如果將上面的for迴圈改為一個死迴圈(對於有些後臺服務程式而言,的確需要死迴圈),那麼DI會建立大量的People物件例項無法被GC及時回收,造成記憶體洩漏。
所以正確使用Transient依賴注入關係的方法應該是,配合IServiceScope物件來使用,我們將上面的程式碼改為如下:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//註冊介面IPeople和類People的關係為Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //執行1000次迴圈,每一次迴圈建立一個People物件例項 for (int i = 0; i < 1000; i++) { //在每一次迴圈中建立一個IServiceScope物件serviceScope,然後使用serviceScope的ServiceProvider建立People物件例項,這樣在rootServiceProvider中並沒有對People物件例項的引用,只有每次迴圈中建立的serviceScope中的ServiceProvider保持了People物件例項的引用,這樣在每次迴圈中當呼叫serviceScope的Dispose方法後,單次迴圈中建立的People物件例項就可以被GC回收了,而不是等到rootServiceProvider呼叫Dispose方法後,才能被GC回收 using (IServiceScope serviceScope = rootServiceProvider.CreateScope()) { IPeople people = serviceScope.ServiceProvider.GetService<IPeople>(); people.DoSomething(); } } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
從上面程式碼中,我們可以看到,由於現在在每次for迴圈中,是由一個獨立的IServiceScope物件serviceScope的ServiceProvider,來建立People物件例項,所以在for迴圈外面的ServiceProvider物件rootServiceProvider,其並沒有內部引用由DI建立的People物件例項。而在每次for迴圈中,我們都呼叫了serviceScope的Dispose方法(也就是在上面第二個using程式碼塊最後),這樣每次迴圈結束後,就沒有任何程式碼引用迴圈內的People物件例項了,GC就可以及時回收由DI建立的People物件例項。
在使用Microsoft.Extensions.DependencyInjection的Transient依賴注入關係時,一定要注意本文所述的記憶體洩漏問題,這個問題可能很多才開始接觸Microsoft.Extensions.DependencyInjection的開發人員不會注意到,但是它會嚴重影響你的程式效能和穩定性。可以參考下面兩篇帖子中發帖人提出的問題:
IServiceProvider garbage collection / disposal
When are .NET Core dependency injected instances disposed?
也可以參考在GitHub上,微軟官方對這個問題的討論:
Revisit tracking transient services for disposal