Come usare ed eseguire il debug di assembly non caricabili in .NET CoreHow to use and debug assembly unloadability in .NET Core

A partire da .NET Core 3.0, è supportata la possibilità di caricare e successivamente scaricare un set di assembly.Starting with .NET Core 3.0, the ability to load and later unload a set of assemblies is supported. In .NET Framework, per questo scopo vengono usati domini di app personalizzati, ma .NET Core supporta un singolo dominio di app predefinito.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 e versioni successive supportano la possibilità di scaricamento tramite la classe AssemblyLoadContext..NET Core 3.0 and later versions support unloadability through AssemblyLoadContext. È possibile caricare un set di assembly in un oggetto AssemblyLoadContext ritirabile, eseguirvi metodi o semplicemente ispezionarli tramite reflection e infine scaricare l'AssemblyLoadContext.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. Che scarica gli assembly AssemblyLoadContextcaricati nel file .That unloads the assemblies loaded into the AssemblyLoadContext.

Esiste una differenza rilevante tra lo scaricamento tramite AssemblyLoadContext e con l'uso di AppDomain.There's one noteworthy difference between the unloading using AssemblyLoadContext and using AppDomains. Con gli AppDomain, lo scaricamento è forzato.With AppDomains, the unloading is forced. In fase di scaricamento, tutti i thread in esecuzione nell'AppDomain di destinazione vengono interrotti, gli oggetti COM gestiti creati nell'AppDomain di destinazione vengono eliminati e così via.At unload time, all threads running in the target AppDomain are aborted, managed COM objects created in the target AppDomain are destroyed, and so on. Con AssemblyLoadContext, lo scaricamento è di tipo "cooperativo".With AssemblyLoadContext, the unload is "cooperative". La chiamata al metodo AssemblyLoadContext.Unload avvia semplicemente lo scaricamento.Calling the AssemblyLoadContext.Unload method just initiates the unloading. Lo scaricamento termina dopo che si sono verificate le condizioni seguenti:The unloading finishes after:

  • Nessuno dei thread include metodi relativi agli assembly caricati nell'AssemblyLoadContext nel rispettivo stack di chiamate.No threads have methods from the assemblies loaded into the AssemblyLoadContext on their call stacks.
  • Nessuno dei tipi degli assembly caricati nelle AssemblyLoadContextistanze di tali tipi e gli assembly stessi fanno riferimento a:None of the types from the assemblies loaded into the AssemblyLoadContext, instances of those types, and the assemblies themselves are referenced by:

Usare AssemblyLoadContext ritirabileUse collectible AssemblyLoadContextUse collectible AssemblyLoadContext

Questa sezione contiene un'esercitazione dettagliata che illustra come caricare in modo semplice un'applicazione .NET Core in un AssemblyLoadContext ritirabile, eseguirne il punto di ingresso e quindi scaricarlo.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. È possibile trovare un https://github.com/dotnet/samples/tree/master/core/tutorials/Unloadingesempio completo in .You can find a complete sample at https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading.

Creare un AssemblyLoadContext ritirabileCreate a collectible AssemblyLoadContext

È necessario derivare la classe dall'AssemblyLoadContext ed eseguire l'overload del relativo metodo AssemblyLoadContext.Load.You need to derive your class from the AssemblyLoadContext and overload its AssemblyLoadContext.Load method. Questo metodo risolve i riferimenti a tutti gli assembly che sono dipendenze degli assembly caricati nell'AssemblyLoadContext.That method resolves references to all assemblies that are dependencies of assemblies loaded into that AssemblyLoadContext.

Il codice seguente offre un esempio della versione più semplice dell'AssemblyLoadContext personalizzato: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;
    }
}

Come si può notare, il metodo Load restituisce null.As you can see, the Load method returns null. Ciò significa che tutti gli assembly di dipendenza vengono caricati nel contesto predefinito e il nuovo contesto contiene solo gli assembly che vi sono stati caricati in modo esplicito.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.

Se si vuole che nell'AssemblyLoadContext vengano caricate anche alcune o tutte le dipendenze, è possibile usare l'oggetto AssemblyDependencyResolver nel metodo Load.If you want to load some or all of the dependencies into the AssemblyLoadContext too, you can use the AssemblyDependencyResolver in the Load method. L'oggetto AssemblyDependencyResolver risolve i nomi degli assiemi in percorsi assoluti dei file di assieme.The AssemblyDependencyResolver resolves the assembly names to absolute assembly file paths. Il sistema di risoluzione utilizza il file .deps.json e i file di assembly nella directory dell'assembly principale caricato nel contesto.The resolver uses the .deps.json file and assembly files in the directory of the main assembly loaded into the context.

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

Usare un AssemblyLoadContext ritirabile personalizzatoUse a custom collectible AssemblyLoadContext

In questa sezione si presuppone che venga usata la versione più semplice dell'TestAssemblyLoadContext.This section assumes the simpler version of the TestAssemblyLoadContext is being used.

È possibile creare un'istanza dell'AssemblyLoadContext personalizzato e caricarvi un assembly, come indicato di seguito: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);

Per ogni assembly a cui fa riferimento l'assembly caricato, viene chiamato il metodo TestAssemblyLoadContext.Load in modo che TestAssemblyLoadContext possa determinare il percorso da cui ottenere l'assembly.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 questo caso, viene restituito null per indicare che deve essere caricato nel contesto predefinito dai percorsi usati dal runtime per caricare gli assembly per impostazione predefinita.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.

Ora che è stato caricato un assembly, è possibile eseguire un metodo da tale contesto.Now that an assembly was loaded, you can execute a method from it. Eseguire il metodo Main:Run the Main method:

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

Dopo che il metodo Main ha restituito un risultato, è possibile avviare lo scaricamento chiamando il metodo Unload sull'AssemblyLoadContext personalizzato o rimuovendo il riferimento all'AssemblyLoadContext: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();

Questa operazione è sufficiente per scaricare l'assembly di test.This is sufficient to unload the test assembly. È possibile includere tutto questo in un metodo separato non abilitato per l'inlining per assicurarsi che TestAssemblyLoadContext, Assembly e MethodInfo (ovvero l'Assembly.EntryPoint) non possano essere mantenuti attivi da riferimenti di slot dello stack (variabili locali reali o introdotte da JIT).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). Tali riferimenti potrebbero mantenere attivi TestAssemblyLoadContext e impedire lo scaricamento.That could keep the TestAssemblyLoadContext alive and prevent the unload.

Restituire inoltre un riferimento debole all'AssemblyLoadContext in modo da poterlo usare in un secondo momento per determinare che lo scaricamento è stato completato.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;
}

A questo punto è possibile eseguire questa funzione per caricare, eseguire e scaricare l'assembly.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);

Lo scaricamento non viene tuttavia completato immediatamente.However, the unload doesn't complete immediately. Come accennato in precedenza, si basa sul Garbage Collector per raccogliere tutti gli oggetti dall'assembly di test.As previously mentioned, it relies on the garbage collector to collect all the objects from the test assembly. In molti casi non è necessario attendere il completamento dello scaricamento.In many cases, it isn't necessary to wait for the unload completion. Vi sono tuttavia casi in cui è utile sapere che l'operazione è stata completata,However, there are cases where it's useful to know that the unload has finished. ad esempio quando è necessario eliminare il file di assembly caricato nell'AssemblyLoadContext personalizzato dal disco.For example, you may want to delete the assembly file that was loaded into the custom AssemblyLoadContext from disk. In casi come questo è possibile usare il frammento di codice seguente.In such a case, the following code snippet can be used. Attiva la procedura di Garbage Collection e attende i finalizzatori AssemblyLoadContext in nullsospeso in un ciclo finché il riferimento debole all'oggetto custom non viene impostato su , a indicare che l'oggetto di destinazione è stato raccolto.It triggers garbage collection 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. Nella maggior parte dei casi, è necessario un solo passaggio attraverso il ciclo.In most cases, just one pass through the loop is required. Tuttavia, per i casi più complessi, in cui gli oggetti creati dal codice in esecuzione nell'AssemblyLoadContext hanno finalizzatori, può essere necessario un numero maggiore di passaggi.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();
}

L'evento di scaricamentoThe Unloading event

Talvolta può essere necessario che il codice caricato in un AssemblyLoadContext personalizzato esegua alcune operazioni di pulizia in fase di avvio dello scaricamento.In some cases, it may be necessary for the code loaded into a custom AssemblyLoadContext to perform some cleanup when the unloading is initiated. Ad esempio, potrebbe essere necessario arrestare i thread o pulire handle GC sicuri.For example, it may need to stop threads or clean up strong GC handles. In questi casi può essere usato l'evento Unloading.The Unloading event can be used in such cases. A questo evento può essere associato un gestore che esegue la pulizia necessaria.A handler that performs the necessary cleanup can be hooked to this event.

Risolvere i problemi di scaricamentoTroubleshoot unloadability issues

A causa della natura cooperativa dello scarico, è facile dimenticare i riferimenti che AssemblyLoadContext possono essere mantenendo viva la roba in un oggetto da collezione e prevenire lo scarico.Due to the cooperative nature of the unloading, it's easy to forget about references that may be keeping the stuff in a collectible AssemblyLoadContext alive and preventing unload. Di seguito è riportato un riepilogo delle entità (alcune delle quali non ovvie) che possono contenere i riferimenti:Here is a summary of entities (some of them non-obvious) that can hold the references:Here is a summary of entities (some of them non-obvious) that can hold the references:

  • Riferimenti regolari conservati dall'esterno AssemblyLoadContext dell'oggetto collectible che vengono archiviati in uno stack slot o in un registro del processore (variabili locali del metodo, create in modo esplicito dal codice utente o implicitamente dal compilatore JIT (Just-In-Time), una variabile statica o una maniglia GC forte (pinning) e che punta in modo transitivo a:Regular references held from outside of the collectible AssemblyLoadContext that are stored in a stack slot or a processor register (method locals, either explicitly created by the user code or implicitly by the just-in-time (JIT) compiler), a static variable, or a strong (pinning) GC handle, and transitively pointing to:
    • Un assembly caricato nell'AssemblyLoadContext ritirabile.An assembly loaded into the collectible AssemblyLoadContext.
    • Un tipo di tale assembly.A type from such an assembly.
    • Un'istanza di un tipo incluso in tale assembly.An instance of a type from such an assembly.
  • Thread che eseguono codice da un assembly caricato nell'AssemblyLoadContext ritirabile.Threads running code from an assembly loaded into the collectible AssemblyLoadContext.
  • Istanze di tipi personalizzati AssemblyLoadContext non ritirabile creati all'interno dell'oggetto da collezione AssemblyLoadContext.Instances of custom, non-collectible AssemblyLoadContext types created inside of the collectible AssemblyLoadContext.
  • Istanze in sospeso RegisteredWaitHandle con callback AssemblyLoadContextimpostati su metodi nell'oggetto custom .Pending RegisteredWaitHandle instances with callbacks set to methods in the custom AssemblyLoadContext.

Suggerimento

I riferimenti agli oggetti archiviati negli slot dello stack o AssemblyLoadContext nei registri del processore e che potrebbero impedire lo scaricamento di un oggetto possono verificarsi nelle seguenti situazioni:Object references that are stored in stack slots or processor registers and that could prevent unloading of an AssemblyLoadContext can occur in the following situations:

  • Quando i risultati della chiamata di funzione vengono passati direttamente a un'altra funzione, anche se non esiste alcuna variabile locale creata dall'utente.When function call results are passed directly to another function, even though there is no user-created local variable.
  • Quando il compilatore JIT mantiene un riferimento a un oggetto che era disponibile a un certo punto in un metodo.When the JIT compiler keeps a reference to an object that was available at some point in a method.

Eseguire il debug dei problemi di scaricamentoDebug unloading issues

Il debug dei problemi relativi allo scaricamento può essere tedioso.Debugging issues with unloading can be tedious. Possono verificarsi situazioni in cui non si riesce a capire ciò che mantiene attivo un AssemblyLoadContext, ma lo scaricamento ha esito negativo.You can get into situations where you don't know what can be holding an AssemblyLoadContext alive, but the unload fails. La strategia migliore per risolvere problemi di questo tipo è quella di usare WinDbg (LLDB su Unix) con il plug-in SOS.The best weapon to help with that is WinDbg (LLDB on Unix) with the SOS plugin. È necessario trovare ciò che mantiene attiva un'istanza di LoaderAllocator appartenente allo specifico AssemblyLoadContext.You need to find what's keeping a LoaderAllocator belonging to the specific AssemblyLoadContext alive. Il plug-in SOS consente di esaminare gli oggetti heap GC, le relative gerarchie e radici.The SOS plugin allows you to look at GC heap objects, their hierarchies, and roots.

Per caricare il plug-in nel debugger, immettere il comando seguente nella riga di comando del debugger:To load the plugin into the debugger, enter the following command in the debugger command line:

In WinDbg (sembra che WinDbg esegua automaticamente questa operazione quando inserisce un'interruzione nell'applicazione .NET Core):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

Esaminiamo un programma di esempio che presenta problemi di scaricamento.Let's debug an example program that has problems with unloading. Di seguito è incluso il codice sorgente.Source code is included below. Quando viene eseguito in WinDbg, il programma si interrompe nel debugger subito dopo il tentativo di verifica dello scaricamento.When you run it under WinDbg, the program breaks into the debugger right after attempting to check for the unload success. A questo punto è possibile iniziare a cercare i colpevoli.You can then start looking for the culprits.

Suggerimento

Se si esegue il debug di LLDB su Unix, i ! comandi SOS negli esempi seguenti non hanno l'inprimoa di essi.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

Questo comando esegue il dump di tutti gli oggetti con un nome di tipo contenente LoaderAllocator che si trovano nell'heap GC.This command dumps all objects with a type name containing LoaderAllocator that are in the GC heap. Esempio: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

Nella sezione "Statistics:" controllare l'oggetto MT (MethodTable) appartenente alla classe System.Reflection.LoaderAllocator, che è l'oggetto a cui si è interessati.In the "Statistics:" part below, check the MT (MethodTable) belonging to the System.Reflection.LoaderAllocator, which is the object we care about. Quindi, nell'elenco all'inizio, trovare MT la voce con corrispondente a quella e ottenere l'indirizzo dell'oggetto stesso.Then, in the list at the beginning, find the entry with MT matching that one and get the address of the object itself. Nel nostro caso, è "000002b78000ce40".In our case, it is "000002b78000ce40".

Ora che conosciamo l'indirizzo dell'oggetto, LoaderAllocator possiamo usare un altro comando per trovare le sue radici GC:Now that we know the address of the LoaderAllocator object, we can use another command to find its GC roots:

!gcroot -all 0x000002b78000ce40

Questo comando esegue il dump della catena di riferimenti agli oggetti che portano all'istanza di LoaderAllocator.This command dumps the chain of object references that lead to the LoaderAllocator instance. L'elenco inizia con la radice, che LoaderAllocator è l'entità che mantiene la nostra vita e quindi è il nucleo del problema.The list starts with the root, which is the entity that keeps our LoaderAllocator alive and thus is the core of the problem. La radice può essere uno slot dello stack, un registro del processore, un handle GC o una variabile statica.The root can be a stack slot, a processor register, a GC handle, or a static variable.

Di seguito è riportato un esempio dell'output del comando gcroot: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.

Il passo successivo è quello di capire dove si trova la radice in modo da poter risolvere il problema.The next step is to figure out where the root is located so you can fix it. Il caso più semplice è quello in cui la radice è uno slot dello stack o un registro del processore.The easiest case is when the root is a stack slot or a processor register. In tal caso, mostra gcroot il nome della funzione il cui frame contiene la radice e il thread che esegue tale funzione.In that case, the gcroot shows the name of the function whose frame contains the root and the thread executing that function. Il caso più complicato è quello in cui la radice è una variabile statica o un handle GC.The difficult case is when the root is a static variable or a GC handle.

Nell'esempio precedente, la prima radice è una variabile locale di tipo System.Reflection.RuntimeMethodInfo memorizzata nel frame della funzione example.Program.Main(System.String[]) all'indirizzo rbp-20 (rbp è il registro del processore rbp e -20 è un offset esadecimale rispetto a tale registro).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).

La seconda radice è un oggetto GCHandle normale (sicuro) che contiene un riferimento a un'istanza della classe test.Test.The second root is a normal (strong) GCHandle that holds a reference to an instance of the test.Test class.

La terza radice è un oggetto GCHandle bloccato.The third root is a pinned GCHandle. Questa è in realtà una variabile statica, ma purtroppo, non c'è modo di dire.This one is actually a static variable, but unfortunately, there is no way to tell. Le entità statiche per i tipi di riferimento vengono memorizzate in una matrice di oggetti gestiti in strutture di runtime interne.Statics for reference types are stored in a managed object array in internal runtime structures.

Un altro caso che può impedire lo scaricamento di un AssemblyLoadContext è quello in cui un thread presenta un frame di un metodo di un assembly caricato nell'AssemblyLoadContext nel relativo stack.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. È possibile verificarlo eseguendo il dump degli stack di chiamate gestite di tutti i thread:You can check that by dumping managed call stacks of all threads:

~*e !clrstack

Questo comando indica di applicare il comando !clrstack a tutti i thread.The command means "apply to all threads the !clrstack command". Di seguito è riportato l'output del comando per l'esempio.The following is the output of that command for the example. Sfortunatamente, LLDB su Unix non ha alcun modo per applicare un comando a clrstack tutti i thread, quindi è necessario cambiare manualmente i thread e ripetere il comando.Unfortunately, LLDB on Unix doesn't have any way to apply a command to all threads, so you must manually switch threads and repeat the clrstack command. Ignorare tutti i thread in cui il debugger dice "Impossibile esaminare lo stack gestito".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]

Come si può notare, nell'ultimo thread è presente test.Program.ThreadProc().As you can see, the last thread has test.Program.ThreadProc(). Si tratta di una funzione dell'assembly caricato nell'AssemblyLoadContext che mantiene quindi attivo l'AssemblyLoadContext.This is a function from the assembly loaded into the AssemblyLoadContext, and so it keeps the AssemblyLoadContext alive.

Esempio di codice sorgente con problemi di scaricamentoExample source with unloadability issues

Il codice seguente viene usato nell'esempio di debug precedente.The following code is used in the previous debugging example.

Programma di test principaleMain 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}");
        }
    }
}

Programma caricato in TestAssemblyLoadContextProgram loaded into the TestAssemblyLoadContext

Il codice seguente rappresenta il file ExecuteAndUnload test.dll passato al metodo nel programma di test principale.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;
        }
    }
}