.NET'te derleme kaldırılabilirliğini kullanma ve hatalarını ayıklama

.NET (Core), bir dizi derlemeyi yükleme ve daha sonra kaldırma özelliğini kullanıma sunar. .NET Framework'te bu amaçla özel uygulama etki alanları kullanılmıştır, ancak .NET (Core) yalnızca tek bir varsayılan uygulama etki alanını destekler.

Kaldırılabilirlik, aracılığıyla AssemblyLoadContextdesteklenir. Bir derleme kümesini bir collectible AssemblyLoadContextiçine yükleyebilir, içinde yöntemler yürütebilir veya yalnızca yansıma kullanarak inceleyebilir ve son olarak öğesini kaldırabilirsiniz AssemblyLoadContext. Bu, içine AssemblyLoadContextyüklenen derlemeleri kaldırır.

AppDomains'i kullanarak AssemblyLoadContext kaldırma ile kullanma arasında önemli bir fark vardır. AppDomains ile kaldırma zorlanır. Yükleme kaldırıldığında, hedef AppDomain'de çalışan tüm iş parçacıkları durduruldu, hedef AppDomain'de oluşturulan yönetilen COM nesneleri yok edilir ve bu şekilde devam eder. ile AssemblyLoadContext, boşaltma "kooperatiftir". yöntemini çağırmak AssemblyLoadContext.Unload yalnızca kaldırma işlemini başlatır. Kaldırma işlemi şu süre sonunda tamamlar:

  • Hiçbir iş parçacığının çağrı yığınlarında içine AssemblyLoadContext yüklenen derlemelerden yöntemleri yoktur.
  • içine yüklenen AssemblyLoadContextderlemelerdeki türlerden hiçbiri, bu türlerin örnekleri ve derlemelerin kendilerine şu şekilde başvuruda bulunur:

Collectible AssemblyLoadContext kullanma

Bu bölüm, bir .NET (Core) uygulamasını bir koleksiyona AssemblyLoadContextyüklemenin, giriş noktasını yürütmenin ve ardından kaldırmanın basit bir yolunu gösteren ayrıntılı bir adım adım öğretici içerir. Tam bir örneği adresinde https://github.com/dotnet/samples/tree/main/core/tutorials/Unloadingbulabilirsiniz.

Collectible AssemblyLoadContext oluşturma

sınıfından AssemblyLoadContext sınıfınızı türetin ve yöntemini geçersiz kılın AssemblyLoadContext.Load . Bu yöntem, içine yüklenen AssemblyLoadContextderlemelerin bağımlılıkları olan tüm derlemelere başvuruları çözümler.

Aşağıdaki kod, en basit özel AssemblyLoadContextörneğidir:

class TestAssemblyLoadContext : AssemblyLoadContext
{
    public TestAssemblyLoadContext() : base(isCollectible: true)
    {
    }

    protected override Assembly? Load(AssemblyName name)
    {
        return null;
    }
}

Load Gördüğünüz gibi yöntemi döndürürnull. Bu, tüm bağımlılık derlemelerinin varsayılan bağlama yüklendiği ve yeni bağlamın yalnızca açıkça yüklenen derlemeleri içerdiği anlamına gelir.

Bağımlılıkların bir kısmını veya tümünü de içine AssemblyLoadContext yüklemek istiyorsanız yöntemini kullanabilirsiniz AssemblyDependencyResolverLoad . , AssemblyDependencyResolver derleme adlarını mutlak derleme dosya yollarına çözümler. Çözümleyici, bağlama yüklenen ana derlemenin dizinindeki .deps.json dosyasını ve derleme dosyalarını kullanır.

using System.Reflection;
using System.Runtime.Loader;

namespace complex
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
        {
            _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
        }

        protected override Assembly? Load(AssemblyName name)
        {
            string? assemblyPath = _resolver.ResolveAssemblyToPath(name);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }
    }
}

Özel bir collectible AssemblyLoadContext kullanma

Bu bölümde, daha basit sürümünün TestAssemblyLoadContext kullanıldığı varsayılır.

Özel AssemblyLoadContext örneğini oluşturabilir ve içine aşağıdaki gibi bir derleme yükleyebilirsiniz:

var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

Yüklenen derleme tarafından başvurulan derlemelerin her biri için yöntemi TestAssemblyLoadContext.Load çağrılır, böylece TestAssemblyLoadContext derlemenin nereden alınacağına karar verebilir. Bu durumda, çalışma zamanının derlemeleri varsayılan olarak yüklemek için kullandığı konumlardan varsayılan bağlama yüklenmesi gerektiğini belirtmek için döndürür null .

Artık bir derleme yüklendiğinden, bundan bir yöntem yürütebilirsiniz. Main yöntemini çalıştırın:

var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);

yöntemi döndürdüğündeMain, özel AssemblyLoadContext üzerindeki yöntemini çağırarak Unload veya öğesine sahip AssemblyLoadContextolduğunuz başvuruyu kaldırarak kaldırma işlemini başlatabilirsiniz:

alc.Unload();

Bu, test derlemesini kaldırmak için yeterlidir. Ardından, yığın yuvası başvuruları (gerçek veya JIT ile tanıtılan yerel öğeler) tarafından , Assemblyve MethodInfo () değerlerinin Assembly.EntryPointcanlı tutulamamasını sağlamak TestAssemblyLoadContextiçin bunların tümünü ayrı bir satırlanamayan yönteme koyacaksınız. Bu, canlı kalmasını TestAssemblyLoadContext sağlayabilir ve boşaltmayı önleyebilir.

Ayrıca, daha sonra kaldırma işlemini algılamak için AssemblyLoadContext kullanabilmeniz için zayıf bir başvuru döndürebilirsiniz.

[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
    var alc = new TestAssemblyLoadContext();
    Assembly a = alc.LoadFromAssemblyPath(assemblyPath);

    alcWeakRef = new WeakReference(alc, trackResurrection: true);

    var args = new object[1] {new string[] {"Hello"}};
    _ = a.EntryPoint?.Invoke(null, args);

    alc.Unload();
}

Artık derlemeyi yüklemek, yürütmek ve kaldırmak için bu işlevi çalıştırabilirsiniz.

WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);

Ancak, kaldırma işlemi hemen tamamlanmaz. Daha önce belirtildiği gibi, test derlemesinden tüm nesneleri toplamak için çöp toplayıcıya dayanır. Çoğu durumda, kaldırma işleminin tamamlanmasını beklemek gerekmez. Ancak, kaldırma işleminin tamamlandığını bilmenin yararlı olduğu durumlar vardır. Örneğin, özel AssemblyLoadContext diske yüklenen derleme dosyasını diskten silmek isteyebilirsiniz. Böyle bir durumda aşağıdaki kod parçacığı kullanılabilir. Çöp toplamayı tetikler ve özel AssemblyLoadContext zayıf başvuru hedef nesnenin toplandığını belirten olarak ayarlanana nullkadar döngüde bekleyen sonlandırıcıları bekler. Çoğu durumda, döngüden yalnızca bir geçiş gereklidir. Ancak, içinde AssemblyLoadContext çalıştırılan kod tarafından oluşturulan nesnelerin sonlandırıcılara sahip olduğu daha karmaşık durumlar için daha fazla geçiş gerekebilir.

for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Kaldırılan olay

Bazı durumlarda, bir özel AssemblyLoadContext dosyaya yüklenen kodun, kaldırma işlemi başlatıldığında biraz temizleme gerçekleştirmesi gerekebilir. Örneğin, iş parçacıklarını durdurması veya güçlü GC tanıtıcılarını temizlemesi gerekebilir. Olay bu Unloading gibi durumlarda kullanılabilir. Bu olay için gerekli temizlemeyi gerçekleştiren bir işleyici bağlayabilirsiniz.

Kaldırılabilirlik sorunlarını giderme

Boşaltmanın işbirlikçi yapısı nedeniyle, eşyaları bir toplama kutusunda AssemblyLoadContext canlı tutan ve boşaltmayı önleyen referansları unutmak kolaydır. Aşağıda, başvuruları tutabilen varlıkların (bazıları gizli olmayan) bir özeti yer alır:

  • Bir yığın yuvasında veya işlemci yazmacında depolanan collectible AssemblyLoadContext dışından tutulan düzenli başvurular (yöntem yerel öğeleri, kullanıcı kodu tarafından açıkça oluşturulmuş veya tam zamanında (JIT) derleyici tarafından örtük olarak), statik bir değişken veya güçlü (sabitleme) GC tutamacı ve geçişli olarak işaret eden:
    • Collectible AssemblyLoadContextiçine yüklenen bir derleme.
    • Böyle bir derlemeden tür.
    • Böyle bir derlemeden tür örneği.
  • Collectible AssemblyLoadContextiçine yüklenen bir derlemeden kod çalıştıran iş parçacıkları.
  • Collectible AssemblyLoadContextiçinde oluşturulan özel, toplanamaz AssemblyLoadContext türlerin örnekleri.
  • Özel içindeki yöntemlere ayarlanmış geri çağırmaları AssemblyLoadContextolan bekleyen RegisteredWaitHandle örnekler.

İpucu

Yığın yuvalarında veya işlemci kayıtlarında depolanan ve 'nin AssemblyLoadContext kaldırılmasını önleyebilecek nesne başvuruları aşağıdaki durumlarda oluşabilir:

  • İşlev çağrısı sonuçları, kullanıcı tarafından oluşturulan yerel değişken olmasa bile doğrudan başka bir işleve geçirildiğinde.
  • JIT derleyicisi bir yöntemin belirli bir noktasında kullanılabilen bir nesneye başvuruyu tuttuğunda.

Kaldırma sorunlarının hatalarını ayıklama

Kaldırmayla ilgili hata ayıklama sorunları yorucu olabilir. Bir canlıyı neyin tutabileceğini AssemblyLoadContext bilmediğiniz, ancak boşaltmanın başarısız olduğu durumlara girebilirsiniz. Bu konuda yardımcı olabilecek en iyi araç, SOS eklentisine sahip WinDbg (veya Unix'te LLDB). Belirli bir LoaderAllocator şeye ait AssemblyLoadContext olanı canlı tutan şeyi bulmalısın. SOS eklentisi GC yığın nesnelerine, hiyerarşilerine ve köklerine bakmanızı sağlar.

SOS eklentisini hata ayıklayıcıya yüklemek için hata ayıklayıcı komut satırına aşağıdaki komutlardan birini girin.

WinDbg'de (henüz yüklenmemişse):

.loadby sos coreclr

LLDB'de:

plugin load /path/to/libsosplugin.so

Şimdi yükleme kaldırma sorunları olan örnek bir programın hatalarını ayıklayacaksınız. Kaynak kodu Örnek kaynak kodu bölümünde bulunur. WinDbg altında çalıştırdığınızda, yükleme kaldırma başarısını denetlemeye çalıştıktan hemen sonra program hata ayıklayıcıya girer. Ardından suçluları aramaya başlayabilirsiniz.

İpucu

Unix'te LLDB kullanarak hata ayıklarsanız, aşağıdaki örneklerde ! yer alan SOS komutlarının önünde yok.

!dumpheap -type LoaderAllocator

Bu komut, GC yığınında bulunan LoaderAllocator bir tür adına sahip tüm nesneleri döküm eder. Bir örnek aşağıda verilmiştir:

         Address               MT     Size
000002b78000ce40 00007ffadc93a288       48
000002b78000ceb0 00007ffadc93a218       24

Statistics:
              MT    Count    TotalSize Class Name
00007ffadc93a218        1           24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288        1           48 System.Reflection.LoaderAllocator
Total 2 objects

"İstatistikler:" bölümünde, ilgilendiğiniz nesne olan öğesine ait System.Reflection.LoaderAllocator() öğesini denetleyin.MTMethodTable Ardından, başlangıçtaki listede, bununla eşleşen girdiyi MT bulun ve nesnenin adresini alın. Bu durumda, "000002b78000ce40" olur.

Artık nesnenin adresini bildiğinize LoaderAllocator göre, GC köklerini bulmak için başka bir komut kullanabilirsiniz:

!gcroot 0x000002b78000ce40

Bu komut, örneğe yol açan nesne başvuruları zincirini döküm eder LoaderAllocator . Liste, canlı tutan LoaderAllocator varlık olan kök ile başlar ve bu nedenle sorunun temelidir. Kök bir yığın yuvası, işlemci yazmacı, GC tutamacı veya statik değişken olabilir.

Aşağıda komutun çıktısının bir örneği verilmişti gcroot :

Thread 4ac:
    000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
        rbp-20: 000000cf9499dd90
            ->  000002b78000d328 System.Reflection.RuntimeMethodInfo
            ->  000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
            ->  000002b78000d1d0 System.RuntimeType
            ->  000002b78000ce40 System.Reflection.LoaderAllocator

HandleTable:
    000002b7f8a81198 (strong handle)
    -> 000002b78000d948 test.Test
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

    000002b7f8a815f8 (pinned handle)
    -> 000002b790001038 System.Object[]
    -> 000002b78000d390 example.TestInfo
    -> 000002b78000d328 System.Reflection.RuntimeMethodInfo
    -> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
    -> 000002b78000d1d0 System.RuntimeType
    -> 000002b78000ce40 System.Reflection.LoaderAllocator

Found 3 roots.

Bir sonraki adım, kökü düzeltebilmeniz için kökün nerede bulunduğunu bulmaktır. En kolay durum, kökün bir yığın yuvası veya işlemci yazmaç olmasıdır. Bu durumda, gcroot çerçevesi kökü içeren işlevin adını ve bu işlevi yürüten iş parçacığını gösterir. Kök statik değişken veya GC tanıtıcısı olduğunda zor bir durum söz konusudur.

Önceki örnekte ilk kök, işlevin çerçevesinde adreste rbp-20 depolanan türün System.Reflection.RuntimeMethodInfo yerel bir köküdür (rbpişlemci yazmacıdır ve -20, bu yazmacın rbp onaltılık uzaklığıdır).example.Program.Main(System.String[])

İkinci kök, sınıfın bir örneğine başvuru tutan normal (güçlü) GCHandle bir kökdür test.Test .

Üçüncü kök sabitlenmiş GCHandlebir . Bu aslında statik bir değişkendir, ancak ne yazık ki bunu anlamanın bir yolu yok. Başvuru türleri için statikler, iç çalışma zamanı yapılarındaki yönetilen nesne dizisinde depolanır.

Bir iş parçacığının AssemblyLoadContext kaldırılmasını önleyebilen bir diğer durum, bir iş parçacığının yığınına yüklenmiş bir derlemeden bir yöntem çerçevesine AssemblyLoadContext sahip olmasıdır. Tüm iş parçacıklarının yönetilen çağrı yığınlarının dökümünü alarak bunu de kontrol edebilirsiniz:

~*e !clrstack

komutu "komutun tüm iş parçacıklarına !clrstack uygula" anlamına gelir. Aşağıda, örnek için bu komutun çıkışı verilmiştir. Ne yazık ki, Unix'te LLDB'nin tüm iş parçacıklarına komut uygulamak için herhangi bir yolu yoktur, bu nedenle iş parçacıklarını el ile değiştirip komutu yinelemeniz clrstack gerekir. Hata ayıklayıcının "Yönetilen yığında adım adım izlenemiyor" sözlerini söylediği tüm iş parçacıklarını yoksayın.

OS Thread Id: 0x6ba8 (0)
        Child SP               IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
        Child SP               IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
        Child SP               IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
        Child SP               IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]

Gördüğünüz gibi, son iş parçacığında vardır test.Program.ThreadProc(). Bu, içine AssemblyLoadContextyüklenen derlemeden bir işlevdir ve bu nedenle canlı kalmasını sağlar AssemblyLoadContext .

Örnek kaynak kodu

Kaldırılabilirlik sorunları içeren aşağıdaki kod, önceki hata ayıklama örneğinde kullanılır.

Ana test programı

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;

namespace example
{
    class TestAssemblyLoadContext : AssemblyLoadContext
    {
        public TestAssemblyLoadContext() : base(true)
        {
        }
        protected override Assembly? Load(AssemblyName name)
        {
            return null;
        }
    }

    class TestInfo
    {
        public TestInfo(MethodInfo? mi)
        {
            _entryPoint = mi;
        }

        MethodInfo? _entryPoint;
    }

    class Program
    {
        static TestInfo? entryPoint;

        [MethodImpl(MethodImplOptions.NoInlining)]
        static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
        {
            var alc = new TestAssemblyLoadContext();
            testAlcWeakRef = new WeakReference(alc);

            Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
            if (a == null)
            {
                testEntryPoint = null;
                Console.WriteLine("Loading the test assembly failed");
                return -1;
            }

            var args = new object[1] {new string[] {"Hello"}};

            // Issue preventing unloading #1 - we keep MethodInfo of a method
            // for an assembly loaded into the TestAssemblyLoadContext in a static variable.
            entryPoint = new TestInfo(a.EntryPoint);
            testEntryPoint = a.EntryPoint;

            var oResult = a.EntryPoint?.Invoke(null, args);
            alc.Unload();
            return (oResult is int result) ? result : -1;
        }

        static void Main(string[] args)
        {
            WeakReference testAlcWeakRef;
            // Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
            MethodInfo? testEntryPoint;
            int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);

            for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }

            System.Diagnostics.Debugger.Break();

            Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
        }
    }
}

TestAssemblyLoadContext içine yüklenen program

Aşağıdaki kod, ana test programında yöntemine ExecuteAndUnload geçirilen test.dll temsil eder.

using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace test
{
    class Test
    {
    }

    class Program
    {
        public static void ThreadProc()
        {
            // Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
            Thread.Sleep(Timeout.Infinite);
        }

        static GCHandle handle;
        static int Main(string[] args)
        {
            // Issue preventing unloading #3 - normal GC handle
            handle = GCHandle.Alloc(new Test());
            Thread t = new Thread(new ThreadStart(ThreadProc));
            t.IsBackground = true;
            t.Start();
            Console.WriteLine($"Hello from the test: args[0] = {args[0]}");

            return 1;
        }
    }
}