1. 程式人生 > >Microsoft.Extensions.DependencyInjection中的Transient依賴注入關係,使用不當會造成記憶體洩漏

Microsoft.Extensions.DependencyInjection中的Transient依賴注入關係,使用不當會造成記憶體洩漏

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

&n