Aracılığıyla paylaş


.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, AssemblyLoadContext aracılığıyla desteklenir. Derlemeler kümesini toplanabilir bir AssemblyLoadContext'e yükleyebilir, içinde yöntemler yürütebilir veya yalnızca yansıma kullanarak inceleyebilir ve son olarak AssemblyLoadContext'i kaldırabilirsiniz. Bu, AssemblyLoadContext içine yüklenen derlemeleri kaldırır.

AssemblyLoadContext kullanarak kaldırma ve AppDomains kullanımı arasında önemli bir fark vardır. AppDomains ile yük boşaltma zorlanır. Yükleme kaldırıldığında, hedef AppDomain'de çalışan tüm iş parçacıkları durdurulur, hedef AppDomain'de oluşturulan yönetilen COM nesneleri yok edilirler ve vesaire. AssemblyLoadContext ile boşaltma "kooperatiftir". AssemblyLoadContext.Unload yöntemini çağırmak yalnızca kaldırma işlemini başlatır. Boşaltma işlemi bu süre sonunda tamamlanır.

  • Hiçbir iş parçacığının çağrı yığınlarında içine AssemblyLoadContext yüklenen derlemelerden yöntemleri yoktur.
  • Yüklenen AssemblyLoadContext derlemelerindeki türlerden hiçbiri, bu türlerin örnekleri ve derlemelerin kendileri tarafından hiçbirine başvurulmamaktadır.

Toplanabilir AssemblyLoadContext kullanma

Bu bölüm, bir .NET (Core) uygulamasını bir toplanabilir AssemblyLoadContext alanına yü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 kılavuz içerir. Tam bir örneği adresinde https://github.com/dotnet/samples/tree/main/core/tutorials/Unloadingbulabilirsiniz.

Collectible AssemblyLoadContext oluşturma

"AssemblyLoadContext sınıfından kendi sınıfınızı türetin ve AssemblyLoadContext.Load yöntemini geçersiz kılın." 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;
    }
}

Gördüğünüz gibi, Load yöntemi null döndürür. 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ü AssemblyLoadContext içine yüklemek istiyorsanız, Load yönteminde AssemblyDependencyResolver kullanabilirsiniz. , 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 toplanabilir 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 her bir derleme için, TestAssemblyLoadContext.Load yöntemi çağrılır, böylece TestAssemblyLoadContext derlemenin nereden alınacağını belirleyebilir. 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 amacıyla null değerini döndürür.

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);

Main yöntemi sona erdiğinde, özel Unload üzerindeki AssemblyLoadContext yöntemini çağırarak veya elinizdeki AssemblyLoadContext başvurusunu kaldırarak yüklemeyi başlatabilirsiniz.

alc.Unload();

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

Daha sonra yük boşaltımının tamamlandığını algılamak için AssemblyLoadContext üzerine zayıf bir referans 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, yük boşaltma 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 nesnesine zayıf referans, hedef nesnenin toplandığını belirten null olarak ayarlanana kadar bir 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();
}

Sınırlamalar

Bir koleksiyona yüklenen derlemeler, AssemblyLoadContexttoplanabilir derlemelerdeki genel kısıtlamalara uymalıdır. Ayrıca aşağıdaki sınırlamalar da geçerlidir:

  • C++/CLI ile yazılan derlemeler desteklenmez.
  • ReadyToRun tarafından oluşturulan kod yoksayılacak.

Boşaltma 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. Bu Unloading etkinlik, böyle durumlarda kullanılabilir. Gereken temizliği yapan bir işleyiciyi bu olaya bağlayabilirsiniz.

Kaldırılabilirlik sorunlarını giderme

Boşaltmanın işbirlikçi doğası nedeniyle, bir kolleksiyon nesnesindeki AssemblyLoadContext eşyaları aktif tutan ve boşaltmayı engelleyen 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 normal başvurular (yöntem yerel değişkenleri, kullanıcı kodu tarafından açıkça oluşturulmuş veya zamanında derleme (JIT) derleyici tarafından örtük olarak), statik bir değişken veya güçlü (sabitleme) GC tutamacı tarafından saklanan ve dolaylı olarak işaret edilen:
    • Collectible AssemblyLoadContext içine yüklenen bir bileşen.
    • Böyle bir derlemeden alınan bir 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.
  • Geri çağırmaları özel AssemblyLoadContext içindeki yöntemlere ayarlanmış RegisteredWaitHandle olan bekleyen örnekler.
  • Özel AssemblyLoadContext alt sınıfınızdaki, yüklenebilir AssemblyLoadContext içerisine yüklenen derlemelere, türlere veya tür örneklerine başvuran alanlar. Boşaltma işlemi devam ederken, çalışma zamanı ortamı boşaltmayı koordine etmek için AssemblyLoadContext üzerinde güçlü bir GC tanıtıcısı tutar. Başka bir deyişle GC, kendi başvurunuzu bıraktıktan sonra bile bu alan başvurularını AssemblyLoadContexttoplamaz. Kaldırma işleminin tamamlayabilmesi için bu alanları temizleyin.

Tavsiye

Yığın yuvalarında veya işlemci kayıtlarında depolanan ve AssemblyLoadContext nesnesinin 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 mevcut olan bu nesneye başvuruyu elinde tuttuğunda.

Boşaltma sorunlarını giderme

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, boşaltma başarısını denetlemeyi denedikten hemen sonra program hata ayıklayıcıya girer. Ardından suçluları aramaya başlayabilirsiniz.

Tavsiye

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

!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, System.Reflection.LoaderAllocator olan ve ilgilenmeniz gereken nesneye ait MT (MethodTable) öğesini denetleyin. 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, LoaderAllocator örneğine yol açan nesne başvuruları zincirini döküm eder. Liste, LoaderAllocator hayatta tutan varlık olan kök ile başlar ve bu nedenle sorunun merkezidir. 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çevesine rbp-20 adresinde depolanan bir yerel türü System.Reflection.RuntimeMethodInfo'dir example.Program.Main(System.String[]). (rbp işlemci yazmacıdır ve -20 bu yazmacın onaltılık offset'idir).

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

Üçü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 boşaltılmasını engelleyebilecek bir diğer durum, iş parçacığının yığında yüklü 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

Bu komut, "!clrstack komutunu tüm iş parçacıklarına 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ığının üzerinde gezinilemiyor" ifadesini belirttiğ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 test.Program.ThreadProc() bulunuyor. Bu, AssemblyLoadContext içine yüklenen derlemedeki bir işlevdir ve bu nedenle AssemblyLoadContext'in canlı kalmasını sağlar.

Örnek kaynak kodu

Yüklenebilirlik sorunları içeren aşağıdaki kod, önceki hata ayıklama örneğinde kullanılmaktadı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

Ana test programındaki bir yönteme test.dll olarak geçirilen aşağıdaki kodu 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;
        }
    }
}