Verwenden und Debuggen von Assemblyentladbarkeit in .NET CoreHow to use and debug assembly unloadability in .NET Core

Ab .NET Core 3.0 wird die Möglichkeit unterstützt, eine Gruppe von Assemblys zu laden und später zu entladen.Starting with .NET Core 3.0, the ability to load and later unload a set of assemblies is supported. In .NET Framework wurden benutzerdefinierte App-Domänen zu diesem Zweck verwendet, .NET Core unterstützt jedoch nur eine einzelne App-Standarddomäne.In .NET Framework, custom app domains were used for this purpose, but .NET Core only supports a single default app domain.

.NET Core 3.0 und höhere Versionen unterstützen Entladbarkeit über AssemblyLoadContext..NET Core 3.0 and later versions support unloadability through AssemblyLoadContext. Sie können eine Gruppe von Assemblys in einen entladbaren AssemblyLoadContext laden, Methoden in ihnen ausführen oder sie einfach mithilfe von Reflektion untersuchen und schließlich den AssemblyLoadContext entladen.You can load a set of assemblies into a collectible AssemblyLoadContext, execute methods in them or just inspect them using reflection, and finally unload the AssemblyLoadContext. Dadurch werden die Assemblys entladen, die in diesen AssemblyLoadContext geladen wurden.That unloads the assemblies loaded into that AssemblyLoadContext.

Es gibt einen bemerkenswerten Unterschied zwischen dem Entladen mithilfe von AssemblyLoadContext und der Verwendung von AppDomains.There's one noteworthy difference between the unloading using AssemblyLoadContext and using AppDomains. Mit AppDomains wird das Entladen erzwungen.With AppDomains, the unloading is forced. Zum Zeitpunkt der Entladung werden beispielsweise alle Threads abgebrochen, die in der Ziel-AppDomain ausgeführt werden. Außerdem werden verwaltete COM-Objekte gelöscht, die in der Ziel-AppDomain erstellt wurden. Mit AssemblyLoadContext ist der Entladevorgang „kooperativ“.At unload time, all threads running in the target AppDomain are aborted, managed COM objects created in the target AppDomain are destroyed, etc. With AssemblyLoadContext, the unload is "cooperative." Durch Aufrufen der AssemblyLoadContext.Unload-Methode wird der Entladevorgang nur initiiert.Calling the AssemblyLoadContext.Unload method just initiates the unloading. Das Entladen wird abgeschlossen, nachdem die folgenden Bedingungen erfüllt sind:The unloading finishes after:

  • Es gibt keine Threads, die über Methoden aus den Assemblys in ihren Aufruflisten verfügen, die in den AssemblyLoadContext geladen werden.No threads have methods from the assemblies loaded into the AssemblyLoadContext on their call stacks.
  • Auf keinen der Typen aus den in den AssemblyLoadContext geladenen Assemblys, auf keine Instanzen dieser Typen und auf keine der Assemblys selbst außerhalb des AssemblyLoadContext wird durch Folgendes verwiesen:None of the types from the assemblies loaded into the AssemblyLoadContext, instances of those types and the assemblies themselves outside of the AssemblyLoadContext are referenced by:

Verwenden des entladbaren „AssemblyLoadContext“-ObjektsUse collectible AssemblyLoadContext

Dieser Abschnitt enthält ein detailliertes schrittweises Tutorial, das eine einfache Möglichkeit zeigt, eine .NET Core-Anwendung in einen entladbaren AssemblyLoadContext zu laden, ihren Einstiegspunkt auszuführen und sie dann zu entladen.This section contains a detailed step-by-step tutorial that shows a simple way to load a .NET Core application into a collectible AssemblyLoadContext, execute its entry point, and then unload it. Das vollständige Beispiel finden Sie unter https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading.You can find a complete sample at https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading.

Erstellen eines entladbaren AssemblyLoadContextCreate a collectible AssemblyLoadContext

Sie müssen die Klasse von AssemblyLoadContext ableiten und ihre AssemblyLoadContext.Load-Methode überladen.You need to derive your class from the AssemblyLoadContext and overload its AssemblyLoadContext.Load method. Diese Methode löst Verweise auf alle Assemblys auf, die Abhängigkeiten von in diesen AssemblyLoadContext geladenen Assemblys sind.That method resolves references to all assemblies that are dependencies of assemblies loaded into that AssemblyLoadContext. Der folgende Code ist ein Beispiel für den einfachsten benutzerdefinierten AssemblyLoadContext:The following code is an example of the simplest custom AssemblyLoadContext:

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

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

Wie Sie sehen können, gibt die Load-Methode null zurück.As you can see, the Load method returns null. Dies bedeutet, dass alle Abhängigkeitsassemblys in den Standardkontext geladen werden und der neue Kontext nur die Assemblys enthält, die explizit in ihn geladen wurden.That means that all the dependency assemblies are loaded into the default context, and the new context contains only the assemblies explicitly loaded into it.

Wenn Sie einige oder alle Abhängigkeiten in den AssemblyLoadContext laden möchten, können Sie AssemblyDependencyResolver in der Load-Methode verwenden.If you want to load some or all of the dependencies into the AssemblyLoadContext too, you can use the AssemblyDependencyResolver in the Load method. AssemblyDependencyResolver löst die Assemblynamen mithilfe der Datei .deps.json, die im Verzeichnis der in den Kontext geladenen Hauptassembly enthalten ist, in absolute Assemblydateipfade auf und verwendet Assemblydateien in diesem Verzeichnis.The AssemblyDependencyResolver resolves the assembly names to absolute assembly file paths using the .deps.json file contained in the directory of the main assembly loaded into the context and using assembly files in that directory.

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

Verwenden eines benutzerdefinierten entladbaren AssemblyLoadContextUse a custom collectible AssemblyLoadContext

In diesem Abschnitt wird davon ausgegangen, dass die einfachere Version von TestAssemblyLoadContext verwendet wird.This section assumes the simpler version of the TestAssemblyLoadContext is being used.

Sie können eine Instanz des benutzerdefinierten AssemblyLoadContext erstellen und eine Assembly wie folgt in diesen laden:You can create an instance of the custom AssemblyLoadContext and load an assembly into it as follows:

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

Für jede der Assemblys, auf die von der geladenen Assembly verwiesen wird, wird die TestAssemblyLoadContext.Load-Methode aufgerufen, sodass der TestAssemblyLoadContext entscheiden kann, von wo die Assembly abgerufen werden soll.For each of the assemblies referenced by the loaded assembly, the TestAssemblyLoadContext.Load method is called so that the TestAssemblyLoadContext can decide where to get the assembly from. In unserem Fall wird null zurückgegeben, um anzugeben, dass sie in den Standardkontext aus Speicherorten geladen werden soll, die die Runtime zum Laden von Assemblys standardmäßig verwendet.In our case, it returns null to indicate that it should be loaded into the default context from locations that the runtime uses to load assemblies by default.

Da nun eine Assembly geladen wurde, können Sie eine Methode daraus ausführen.Now that an assembly was loaded, you can execute a method from it. Führen Sie die Main-Methode aus:Run the Main method:

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

Nachdem die Main-Methode abgeschlossen wurde, können Sie das Entladen einleiten, indem Sie entweder die Unload-Methode für den benutzerdefinierten AssemblyLoadContext aufrufen oder den Verweis auf den AssemblyLoadContext entfernen:After the Main method returns, you can initiate unloading by either calling the Unload method on the custom AssemblyLoadContext or getting rid of the reference you have to the AssemblyLoadContext:

alc.Unload();

Dies reicht aus, um die Testassembly zu entladen.This is sufficient to unload the test assembly. Bringen wird all dies in einer separaten, nicht inlinefähigen Methode zusammen, um sicherzustellen, dass TestAssemblyLoadContext, Assembly und MethodInfo (der Assembly.EntryPoint) nicht durch Stackslotverweise (reale oder durch JIT eingeführte lokale Variablen) aktiv gehalten werden.Let's actually put all of this into a separate non-inlineable method to ensure that the TestAssemblyLoadContext, Assembly, and MethodInfo (the Assembly.EntryPoint) can't be kept alive by stack slot references (real- or JIT-introduced locals). Dies könnte den TestAssemblyLoadContext aktiv lassen und das Entladen verhindern.That could keep the TestAssemblyLoadContext alive and prevent the unload.

Geben Sie außerdem einen schwachen Verweis auf den AssemblyLoadContext zurück, damit Sie ihn später verwenden können, um den Abschluss des Entladevorgangs zu erkennen.Also, return a weak reference to the AssemblyLoadContext so that you can use it later to detect unload completion.

[MethodImpl(MethodImplOptions.NoInlining)]
static int 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"}};
    int result = (int)a.EntryPoint.Invoke(null, args);

    alc.Unload();

    return result;
}

Nun können Sie diese Funktion ausführen, um die Assembly zu laden, auszuführen und zu entladen.Now you can run this function to load, execute, and unload the assembly.

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

Das Entladen wird jedoch nicht sofort beendet.However, the unload doesn't complete immediately. Wie bereits erwähnt, verwendet der Vorgang GC, um alle Objekte aus der Testassembly zu erfassen.As previously mentioned, it relies on the GC to collect all the objects from the test assembly. In vielen Fällen ist es nicht erforderlich, auf den Abschluss des Entladevorgangs zu warten.In many cases, it isn't necessary to wait for the unload completion. Es gibt jedoch Fälle, in denen es nützlich zu wissen ist, dass der Entladevorgang abgeschlossen wurde.However, there are cases where it's useful to know that the unload has finished. Beispielsweise, wenn Sie die Assemblydatei löschen möchten, die vom Datenträger in den benutzerdefinierten AssemblyLoadContext geladen wurde.For example, you may want to delete the assembly file that was loaded into the custom AssemblyLoadContext from disk. In einem solchen Fall kann der folgende Codeausschnitt verwendet werden.In such a case, the following code snippet can be used. Er löst eine GC aus und wartet auf ausstehende Finalizer in einer Schleife, bis der schwache Verweis auf den benutzerdefinierten AssemblyLoadContext auf null festgelegt wurde, um anzugeben, dass das Zielobjekt entladen wurde.It triggers a GC and waits for pending finalizers in a loop until the weak reference to the custom AssemblyLoadContext is set to null, indicating the target object was collected. Beachten Sie, dass in den meisten Fällen nur ein Durchlauf durch die Schleife erforderlich ist.Note that in most cases, just one pass through the loop is required. Für komplexere Fälle, in denen Objekte, die durch den im AssemblyLoadContext ausgeführten Code erstellt wurden, über Finalizer verfügen, sind möglicherweise weitere Durchgänge erforderlich.However, for more complex cases where objects created by the code running in the AssemblyLoadContext have finalizers, more passes may be needed.

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

Das Unloading-EreignisThe Unloading event

In einigen Fällen kann es erforderlich sein, dass der Code, der in einen benutzerdefinierten AssemblyLoadContext geladen wird, einige Bereinigungsaufgaben durchführt, wenn das Entladen initiiert wird.In some cases, it may be necessary for the code loaded into a custom AssemblyLoadContext to perform some cleanup when the unloading is initiated. Beispielsweise kann es erforderlich sein, Threads anzuhalten und einige starke GC-Handles zu bereinigen. Das Unloading-Ereignis kann in solchen Fällen verwendet werden.For example, it may need to stop threads, clean up some strong GC handles, etc. The Unloading event can be used in such cases. Ein Handler, der die erforderliche Bereinigung ausführt, kann mit diesem Ereignis verknüpft werden.A handler that performs the necessary cleanup can be hooked to this event.

Behandlung bei Problemen mit der EntladbarkeitTroubleshoot unloadability issues

Aufgrund der kooperativen Natur der Entladung kann leicht vergessen werden, dass Verweise die Objekte in einem entladbaren AssemblyLoadContext aktiv halten und das Entladen verhindern können.Due to the cooperative nature of the unloading, it's easy to forget about references keeping the stuff in a collectible AssemblyLoadContext alive and preventing unload. Hier ist eine Zusammenfassung der Objekte (einige davon nicht offensichtlich), die die Verweise enthalten können:Here is a summary of things (some of them non-obvious) that can hold the references:

  • Reguläre Verweise, die außerhalb des entladbaren AssemblyLoadContext enthalten und in einem Stackslot oder einem Prozessorregister gespeichert sind (lokale Variablen von Methoden, entweder explizit durch den Benutzercode oder implizit durch JIT erstellt), eine statische Variable oder ein starkes/fixierendes GC-Handle und Transitivität, die auf Folgendes verweist:Regular references held from outside of the collectible AssemblyLoadContext, stored in a stack slot or a processor register (method locals, either explicitly created by the user code or implicitly by the JIT), a static variable or a strong / pinning GC handle, and transitively pointing to:
    • Eine Assembly, die in den entladbaren AssemblyLoadContext geladen wurde.An assembly loaded into the collectible AssemblyLoadContext.
    • Ein Typ aus einer solchen Assembly.A type from such an assembly.
    • Eine Instanz eines Typs aus einer solchen Assembly.An instance of a type from such an assembly.
  • Threads, die Code aus einer Assembly ausführen, die in den entladbaren AssemblyLoadContext geladen wurde.Threads running code from an assembly loaded into the collectible AssemblyLoadContext.
  • Instanzen von benutzerdefinierten, nicht entladbaren AssemblyLoadContext-Typen, die im entladbaren AssemblyLoadContext erstellt wurden.Instances of custom non-collectible AssemblyLoadContext types created inside of the collectible AssemblyLoadContext
  • Ausstehende RegisteredWaitHandle-Instanzen, bei denen Rückrufe auf Methoden im benutzerdefinierten AssemblyLoadContext festgelegt sind.Pending RegisteredWaitHandle instances with callbacks set to methods in the custom AssemblyLoadContext

Hinweise zum Auffinden von Stackslots/Prozessorregistern, die ein Stammobjekt definieren:Hints to find stack slot / processor register rooting an object:

  • Wenn Sie Ergebnisse eines Funktionsaufrufs direkt an eine andere Funktion übergeben, kann selbst dann ein Stamm erstellt werden, wenn keine vom Benutzer erstellte lokale Variable vorhanden ist.Passing function call results directly to another function may create a root even though there is no user-created local variable.
  • Wenn zu irgendeinem Zeitpunkt in einer Methode ein Verweis auf ein Objekt verfügbar war, kann das JIT ggf. entscheiden, den Verweis in einem Stackslot/Prozessorregister so lange wie gewünscht in der aktuellen Funktion zu speichern.If a reference to an object was available at any point in a method, the JIT might have decided to keep the reference in a stack slot / processor register for as long as it wants in the current function.

Debuggen von Problemen beim EntladenDebug unloading issues

Das Debuggen von Problemen beim Entladen kann mühsam sein.Debugging issues with unloading can be tedious. Es können Situationen eintreten, in denen Sie nicht wissen, was einen AssemblyLoadContext aktiv halten könnte, aber das Entladen schlägt fehl.You can get into situations where you don't know what can be holding an AssemblyLoadContext alive, but the unload fails. Die beste Waffe bei einem solchen Problem ist WinDbg (LLDB unter Unix) mit dem SOS-Plug-In.The best weapon to help with that is WinDbg (LLDB on Unix) with the SOS plugin. Sie müssen herausfinden, was einen LoaderAllocator, der zu dem jeweiligen AssemblyLoadContext gehört, aktiv bleiben lässt.You need to find what's keeping a LoaderAllocator belonging to the specific AssemblyLoadContext alive. Mit diesem Plug-In können Sie GC-Heapobjekte, ihre Hierarchien und Stämme überprüfen.This plugin allows you to look at GC heap objects, their hierarchies, and roots. Um das Plug-In in den Debugger zu laden, geben Sie den folgenden Befehl in der Debuggerbefehlszeile ein:To load the plugin into the debugger, enter the following command in the debugger command line:

In WinDbg (in WinDbg scheint dies automatisch beim Einstieg in die .NET Core-Anwendung zu geschehen):In WinDbg (it seems WinDbg does that automatically when breaking into .NET Core application):

.loadby sos coreclr

In LLDB:In LLDB:

plugin load /path/to/libsosplugin.so

Versuchen wir, ein Beispielprogramm zu debuggen, das Probleme beim Entladen hat.Let's try to debug an example program that has problems with unloading. Der Quellcode ist unten enthalten.Source code is included below. Wenn Sie ihn unter WinDbg ausführen, steigt das Programm direkt nach dem Versuch, den Erfolg des Entladevorgangs zu überprüfen, in den Debugger ein.When you run it under WinDbg, the program breaks into the debugger right after attempting to check for the unload success. Sie können dann mit der Suche nach den „Schuldigen“ beginnen.You can then start looking for the culprits.

Beachten Sie Folgendes: Wenn Sie mit LLDB unter UNIX debuggen, wird den SOS-Befehlen in den folgenden Beispielen kein ! vorangestellt.Note that if you debug using LLDB on Unix, the SOS commands in the following examples don't have the ! in front of them.

!dumpheap -type LoaderAllocator

Dieser Befehl sichert alle Objekte mit einem Typnamen, der LoaderAllocator enthält, aus dem GC-Heap.This command dumps all objects with a type name containing LoaderAllocator that are in the GC heap. Im Folgenden ein Beispiel:Here is an example:

         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

Überprüfen Sie im Abschnitt „Statistics“ unten die MT (MethodTable), die zu System.Reflection.LoaderAllocator gehört. Dies ist das Objekt, für das wir uns interessieren.In the "Statistics:" part below, check the MT (MethodTable) belonging to the System.Reflection.LoaderAllocator, which is the object we care about. Suchen Sie dann in der Liste am Anfang den Eintrag mit MT, der diesem Eintrag entspricht, und rufen Sie die Adresse des Objekts selbst ab.Then in the list at the beginning, find the entry with MT matching that one and get the address of the object itself. In unserem Fall lautet der Wert „000002b78000ce40“.In our case, it is "000002b78000ce40"

Nachdem wir nun die Adresse des LoaderAllocator-Objekts kennen, können wir einen anderen Befehl verwenden, um seine GC-Stämme zu finden.Now that we know the address of the LoaderAllocator object, we can use another command to find its GC roots

!gcroot -all 0x000002b78000ce40 

Dieser Befehl sichert die Kette von Objektverweisen, die zur LoaderAllocator-Instanz führen.This command dumps the chain of object references that lead to the LoaderAllocator instance. Die Liste beginnt mit dem Stamm, bei dem es sich um die Entität handelt, die den LoaderAllocator aktiv hält und daher den Kern des Problems darstellt, das Sie debuggen.The list starts with the root, which is the entity that keeps our LoaderAllocator alive and thus is the core of the problem you're debugging. Der Stamm kann ein Stackslot, ein Prozessorregister, ein GC-Handle oder eine statische Variable sein.The root can be a stack slot, a processor register, a GC handle, or a static variable.

Hier ist ein Beispiel für die Ausgabe des gcroot-Befehls:Here is an example of the output of the gcroot command:

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.

Nun müssen Sie herausfinden, wo sich der Stamm befindet, damit Sie das Problem beheben können.Now you need to figure out where the root is located so you can fix it. Im einfachsten Fall ist der Stamm ein Stackslot oder ein Prozessorregister.The easiest case is when the root is a stack slot or a processor register. In diesem Fall zeigt gcroot den Namen der Funktion an, deren Frame den Stamm und den Thread enthält, der diese Funktion ausführt.In that case, the gcroot shows you the name of the function whose frame contains the root and the thread executing that function. Ein schwieriger Fall liegt vor, wenn der Stamm eine statische Variable oder ein GC-Handle ist.The difficult case is when the root is a static variable or a GC handle.

Im vorherigen Beispiel ist der erste Stamm eine lokale Variable vom Typ System.Reflection.RuntimeMethodInfo, die im Frame der Funktion example.Program.Main(System.String[]) bei Adresse rbp-20 gespeichert ist (rbp ist das Prozessorregister rbp, und -20 ist ein hexadezimaler Offset von diesem Register).In the previous example, the first root is a local of type System.Reflection.RuntimeMethodInfo stored in the frame of the function example.Program.Main(System.String[]) at address rbp-20 (rbp is the processor register rbp and -20 is a hexadecimal offset from that register).

Der zweite Stamm ist ein normales (starkes) GCHandle, das einen Verweis auf eine Instanz der test.Test-Klasse enthält.The second root is a normal (strong) GCHandle that holds a reference to an instance of the test.Test class.

Der dritte Stamm ist ein fixiertes GCHandle.The third root is a pinned GCHandle. Dabei handelt es sich tatsächlich um eine statische Variable.This one is actually a static variable. Leider gibt es keine Möglichkeit, dies festzustellen.Unfortunately, there is no way to tell. Statische Variablen für Verweistypen werden in einem verwalteten Objektarray in internen Laufzeitstrukturen gespeichert.Statics for reference types are stored in a managed object array in internal runtime structures.

Ein weiterer Fall, der das Entladen eines AssemblyLoadContext verhindern kann: Wenn ein Thread über einen Frame einer Methode aus einer Assembly verfügt, die in den AssemblyLoadContext auf ihrem Stapel geladen wurde.Another case that can prevent unloading of an AssemblyLoadContext is when a thread has a frame of a method from an assembly loaded into the AssemblyLoadContext on its stack. Sie können dies überprüfen, indem Sie die verwalteten Aufruflisten aller Threads sichern:You can check that by dumping managed call stacks of all threads:

~*e !clrstack

Der Befehl bedeutet „Befehl !clrstack auf alle Threads anwenden“.The command means "apply to all threads the !clrstack command". Die folgende Abbildung zeigt die Ausgabe dieses Befehls für das Beispiel.The following is the output of that command for the example. Leider bietet LLDB unter Unix keine Möglichkeit, einen Befehl auf alle Threads anzuwenden, sodass Sie die Threads manuell wechseln und den Befehl clrstack wiederholen müssen.Unfortunately, LLDB on Unix doesn't have any way to apply a command to all threads, so you'll need to resort to manually switching threads and repeating the clrstack command. Ignorieren Sie alle Threads, bei denen der Debugger „Unable to walk the managed stack“ (Verwalteter Stapel kann nicht durchlaufen werden) anzeigt.Ignore all threads where the debugger says "Unable to walk the managed stack."

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] 

Wie Sie erkennen können, weist der letzte Thread test.Program.ThreadProc() auf.As you can see, the last thread has test.Program.ThreadProc(). Dabei handelt es sich um eine Funktion aus der Assembly, die in den AssemblyLoadContext geladen wird, sodass der AssemblyLoadContext aktiv bleibt.This is a function from the assembly loaded into the AssemblyLoadContext, and so it keeps the AssemblyLoadContext alive.

Beispielquellcode mit Problemen bei der EntladbarkeitExample source with unloadability issues

Der folgende Code wird im vorherigen Debugbeispiel verwendet.The following code is used in the previous debugging example.

HaupttestprogrammMain testing 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;

            int result = (int)a.EntryPoint.Invoke(null, args);
            alc.Unload();

            return result;
        }

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

In den TestAssemblyLoadContext geladenes ProgrammProgram loaded into the TestAssemblyLoadContext

Der folgende Code stellt die Datei test.dll dar, die der ExecuteAndUnload-Methode im Haupttestprogramm übergeben wird.The following code represents the test.dll passed to the ExecuteAndUnload method in the main testing program.

using System;
using System.Runtime.InteropServices;

namespace test
{
    class Test
    {
        string message = "Hello";
    }

    class Program
    {
        public static void ThreadProc()
        {
            // Issue preventing unlopading #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;
        }
    }
}