Share via


De laadbaarheid van assembly's gebruiken en fouten opsporen in .NET

.NET (Core) heeft de mogelijkheid geïntroduceerd om een set assembly's te laden en later te verwijderen. In .NET Framework zijn hiervoor aangepaste app-domeinen gebruikt, maar .NET (Core) ondersteunt slechts één standaard-app-domein.

De laadbaarheid wordt ondersteund via AssemblyLoadContext. U kunt een set assembly's in een verzamelbare AssemblyLoadContextverzameling laden, methoden erin uitvoeren of ze gewoon inspecteren met weerspiegeling en ten slotte de AssemblyLoadContext. Hiermee worden de assembly's die in het bestand zijn geladen, uitgepakt AssemblyLoadContext.

Er is één opmerkelijk verschil tussen het lossen met behulp van AssemblyLoadContext en het gebruik van AppDomains. Met AppDomains wordt het lossen gedwongen. Bij het uitladen worden alle threads die in het doel-AppDomain worden uitgevoerd, afgebroken, beheerde COM-objecten die in het doel-AppDomain zijn gemaakt, vernietigd, enzovoort. Met AssemblyLoadContext, de ontlading is "coöperatie". Als u de methode aanroept, wordt het AssemblyLoadContext.Unload lossen gestart. Het lossen is voltooid na:

  • Er zijn geen threads die methoden hebben van de assembly's die in de AssemblyLoadContext aanroepstacks zijn geladen.
  • Geen van de typen van de assembly's die in de AssemblyLoadContextassembly's zijn geladen, exemplaren van deze typen en de assembly's zelf worden verwezen door:

Collectible AssemblyLoadContext gebruiken

Deze sectie bevat een gedetailleerde stapsgewijze zelfstudie waarin een eenvoudige manier wordt getoond om een .NET-toepassing (Core) in een verzamelbare AssemblyLoadContexttoepassing te laden, het ingangspunt uit te voeren en het vervolgens te verwijderen. U vindt een volledig voorbeeld op https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.

Een collectible AssemblyLoadContext maken

Uw klas afleiden van de AssemblyLoadContext methode en deze overschrijven AssemblyLoadContext.Load . Deze methode lost verwijzingen op naar alle assembly's die afhankelijkheden zijn van assembly's die in dat AssemblyLoadContextbestand zijn geladen.

De volgende code is een voorbeeld van de eenvoudigste aangepaste AssemblyLoadContextcode:

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

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

Zoals u ziet, retourneert nullde Load methode . Dit betekent dat alle afhankelijkheidsassembly's in de standaardcontext worden geladen en dat de nieuwe context alleen de assembly's bevat die expliciet in de assembly's zijn geladen.

Als u bepaalde of alle afhankelijkheden in de AssemblyLoadContext app wilt laden, kunt u de AssemblyDependencyResolverLoad methode gebruiken. Hiermee AssemblyDependencyResolver worden de assemblynamen omgezet in absolute assemblybestandspaden. De resolver gebruikt de .deps.json bestands- en assemblybestanden in de map van de hoofdassembly die in de context is geladen.

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

Een aangepaste collectible AssemblyLoadContext gebruiken

In deze sectie wordt ervan uitgegaan dat de eenvoudigere versie van de TestAssemblyLoadContext versie wordt gebruikt.

U kunt als volgt een exemplaar van de aangepaste AssemblyLoadContext maken en een assembly erin laden:

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

Voor elk van de assembly's waarnaar wordt verwezen door de geladen assembly, wordt de TestAssemblyLoadContext.Load methode aangeroepen zodat de TestAssemblyLoadContext assembly kan bepalen waar de assembly vandaan komt. In dit geval wordt geretourneerd null om aan te geven dat deze moet worden geladen in de standaardcontext vanaf locaties die door de runtime worden gebruikt om standaard assembly's te laden.

Nu een assembly is geladen, kunt u er een methode van uitvoeren. Voer de Main methode uit:

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

Nadat de Main methode is geretourneerd, kunt u het lossen initiëren door de Unload methode aan te roepen op de aangepaste AssemblyLoadContext methode of door de verwijzing naar het AssemblyLoadContextvolgende te verwijderen:

alc.Unload();

Dit is voldoende om de testassembly te ontladen. Vervolgens plaatst u dit allemaal in een afzonderlijke niet-inlineerbare methode om ervoor te zorgen dat de TestAssemblyLoadContext, Assemblyen MethodInfo (de Assembly.EntryPoint) niet kunnen worden bewaard door stack-siteverwijzingen (echte of door JIT geïntroduceerde lokale bevolking). Dat kan het TestAssemblyLoadContext leven behouden en het ontladen voorkomen.

Retourneer ook een zwakke verwijzing naar de AssemblyLoadContext zodat u deze later kunt gebruiken om de voltooiing van het laden te detecteren.

[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();
}

U kunt deze functie nu uitvoeren om de assembly te laden, uit te voeren en te ontladen.

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

Het uitladen wordt echter niet onmiddellijk voltooid. Zoals eerder vermeld, is het afhankelijk van de garbagecollector om alle objecten van de testassembly te verzamelen. In veel gevallen is het niet nodig om te wachten totdat het laden is voltooid. Er zijn echter gevallen waarin het handig is om te weten dat het verwijderen is voltooid. U kunt bijvoorbeeld het assemblybestand verwijderen dat is geladen in de aangepaste AssemblyLoadContext schijf. In dat geval kan het volgende codefragment worden gebruikt. Het activeert garbagecollection en wacht op in behandeling zijnde finalizers in een lus totdat de zwakke verwijzing naar de aangepaste AssemblyLoadContext is ingesteld nullop , waarmee wordt aangegeven dat het doelobject is verzameld. In de meeste gevallen is slechts één pass through de lus vereist. Voor complexere gevallen waarin objecten die zijn gemaakt door de code die wordt uitgevoerd in de AssemblyLoadContext have finalizers, zijn er echter mogelijk meer passen nodig.

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

Beperkingen

Assembly's die in een verzamelobject AssemblyLoadContext zijn geladen, moeten voldoen aan de algemene beperkingen voor verzamelbare assembly's. De volgende beperkingen zijn ook van toepassing:

  • Assembly's die zijn geschreven in C++/CLI worden niet ondersteund.
  • ReadyToRun gegenereerde code wordt genegeerd.

Het losmaken van het evenement

In sommige gevallen kan het nodig zijn voor de code die in een aangepaste AssemblyLoadContext code is geladen om een opschoonbewerking uit te voeren wanneer het lossen wordt gestart. Het kan bijvoorbeeld nodig zijn om threads te stoppen of sterke GC-ingangen op te schonen. De Unloading gebeurtenis kan in dergelijke gevallen worden gebruikt. U kunt een handler koppelen die de benodigde opschoonactie voor deze gebeurtenis uitvoert.

Problemen met uitlaadbaarheid oplossen

Vanwege de coöperatieve aard van het lossen is het gemakkelijk om verwijzingen te vergeten die het spul in een verzamelbaar AssemblyLoadContext leven kunnen houden en het ontladen verhinderen. Hier volgt een samenvatting van entiteiten (sommige hiervan niet-obvious) die de verwijzingen kunnen bevatten:

  • Reguliere verwijzingen van buiten het verzamelobject AssemblyLoadContext die zijn opgeslagen in een stacksite of een processorregister (methode locals, expliciet gemaakt door de gebruikerscode of impliciet door de Just-In-Time-compiler (JIT) compiler, een statische variabele of een sterke (pinning) GC-ingang, en transitief verwijst naar:
    • Een assembly die in het verzamelobject AssemblyLoadContextis geladen.
    • Een type van een dergelijke assembly.
    • Een exemplaar van een type van een dergelijke assembly.
  • Threads die code uitvoeren vanuit een assembly die in het verzamelobject AssemblyLoadContextis geladen.
  • Exemplaren van aangepaste, niet-verzamelbare AssemblyLoadContext typen die zijn gemaakt in de verzamelbare AssemblyLoadContext.
  • In behandeling zijnde RegisteredWaitHandle exemplaren met callbacks ingesteld op methoden in de aangepaste AssemblyLoadContext.

Tip

Objectverwijzingen die zijn opgeslagen in stacksleuven of processorregisters en die kunnen voorkomen dat er een AssemblyLoadContext probleem kan worden verwijderd, kunnen zich in de volgende situaties voordoen:

  • Wanneer resultaten van functieaanroep rechtstreeks worden doorgegeven aan een andere functie, ook al is er geen door de gebruiker gemaakte lokale variabele.
  • Wanneer de JIT-compiler een verwijzing houdt naar een object dat op een bepaald moment in een methode beschikbaar was.

Losproblemen oplossen

Foutopsporingsproblemen bij het lossen kunnen vervelend zijn. U kunt in situaties komen waarin u niet weet wat er AssemblyLoadContext levend kan worden vastgehouden, maar het uitladen mislukt. Het beste hulpprogramma om daarmee te helpen is WinDbg (of LLDB op Unix) met de SOS-invoegtoepassing. Je moet vinden wat een LoaderAllocator deel uitmaakt van het specifieke AssemblyLoadContext levend. Met de SOS-invoegtoepassing kunt u GC-heapobjecten, hun hiërarchieën en wortels bekijken.

Als u de SOS-invoegtoepassing wilt laden in het foutopsporingsprogramma, voert u een van de volgende opdrachten in de opdrachtregel voor foutopsporingsprogramma in.

In WinDbg (als deze nog niet is geladen):

.loadby sos coreclr

In LLDB:

plugin load /path/to/libsosplugin.so

U gaat nu fouten opsporen in een voorbeeldprogramma dat problemen heeft met het lossen. De broncode is beschikbaar in de sectie Voorbeeldbroncode . Wanneer u het uitvoert onder WinDbg, breekt het programma in het foutopsporingsprogramma in nadat is geprobeerd te controleren of het laden is gelukt. Vervolgens kunt u op zoek gaan naar de schuldigen.

Tip

Als u fouten opssport met behulp van LLDB op Unix, hebben de SOS-opdrachten in de volgende voorbeelden niet de ! voorzijde.

!dumpheap -type LoaderAllocator

Met deze opdracht worden alle objecten gedumpt met een typenaam LoaderAllocator die zich in de GC-heap bevindt. Hier volgt een voorbeeld:

         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

Controleer in het gedeelte Statistieken:de MT (MethodTable) die deel uitmaakt van het System.Reflection.LoaderAllocatorobject waar u om geeft. Zoek vervolgens in de lijst aan het begin de vermelding met MT die vermelding en haal het adres van het object zelf op. In dit geval is het '000002b78000ce40'.

Nu u het adres van het LoaderAllocator object kent, kunt u een andere opdracht gebruiken om de GC-wortels te vinden:

!gcroot 0x000002b78000ce40

Met deze opdracht wordt de keten van objectverwijzingen gedumpt die naar het LoaderAllocator exemplaar leiden. De lijst begint met de hoofdmap, die de entiteit is die het LoaderAllocator levend houdt en dus de kern van het probleem is. De hoofdmap kan een stacksite, een processorregister, een GC-ingang of een statische variabele zijn.

Hier volgt een voorbeeld van de uitvoer van de gcroot opdracht:

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.

De volgende stap is om erachter te komen waar de hoofdmap zich bevindt, zodat u deze kunt oplossen. Het eenvoudigste geval is wanneer de hoofdmap een stacksite of een processorregister is. In dat geval gcroot ziet u de naam van de functie waarvan het frame de hoofdmap en de thread bevat die die functie uitvoert. Het moeilijke geval is wanneer de hoofdmap een statische variabele of een GC-ingang is.

In het vorige voorbeeld is de eerste hoofdmap een lokaal type System.Reflection.RuntimeMethodInfo dat is opgeslagen in het frame van de functie example.Program.Main(System.String[]) op adres rbp-20 (rbp is het processorregister rbp en -20 een hexadecimale offset van dat register).

De tweede hoofdmap is een normale (sterke) GCHandle die een verwijzing naar een exemplaar van de test.Test klasse bevat.

De derde hoofdmap is een vastgemaakt GCHandle. Deze is eigenlijk een statische variabele, maar helaas is er geen manier om het te vertellen. Statische gegevens voor referentietypen worden opgeslagen in een beheerde objectmatrix in interne runtimestructuren.

Een ander geval dat het lossen van een draad AssemblyLoadContext kan voorkomen, is wanneer een draad een frame van een methode van een assembly heeft die in de AssemblyLoadContext stapel is geladen. U kunt dit controleren door beheerde aanroepstacks van alle threads te dumpen:

~*e !clrstack

De opdracht betekent 'toepassen op alle threads van de !clrstack opdracht'. Hier volgt de uitvoer van die opdracht voor het voorbeeld. Helaas heeft LLDB op Unix geen manier om een opdracht toe te passen op alle threads, dus u moet threads handmatig wisselen en de clrstack opdracht herhalen. Negeer alle threads waarin het foutopsporingsprogramma 'Kan de beheerde stack niet doorlopen'.

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]

Zoals u kunt zien, heeft test.Program.ThreadProc()de laatste thread . Dit is een functie van de assembly die in de AssemblyLoadContextassembly is geladen, en dus blijft het AssemblyLoadContext leven.

Voorbeeld van broncode

De volgende code die problemen met de laadbaarheid bevat, wordt gebruikt in het vorige voorbeeld van foutopsporing.

Hoofdtestprogramma

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

Het programma is geladen in de TestAssemblyLoadContext

De volgende code vertegenwoordigt de test.dll doorgegeven aan de ExecuteAndUnload methode in het hoofdtestprogramma.

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