Comment utiliser et déboguer la non-chargeabilité d’assembly dans .NET CoreHow to use and debug assembly unloadability in .NET Core

À compter de .NET Core 3.0, il est possible de charger un ensemble d’assemblys et de le décharger plus tard.Starting with .NET Core 3.0, the ability to load and later unload a set of assemblies is supported. Dans le .NET Framework, les domaines d’application personnalisés étaient utilisés à cet effet, mais .NET Core ne prend en charge qu’un seul domaine d’application par défaut.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 et versions ultérieures utilisent AssemblyLoadContext pour prendre en charge le déchargement..NET Core 3.0 and later versions support unloadability through AssemblyLoadContext. Vous pouvez charger un ensemble d’assemblys dans un AssemblyLoadContext pouvant être collecté, exécuter des méthodes dans ceux-ci ou simplement les inspecter par réflexion, puis finalement décharger 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. Cela décharge les assemblys chargés dans le AssemblyLoadContext .That unloads the assemblies loaded into the AssemblyLoadContext.

Il existe une différence notable entre le déchargement qui utilise AssemblyLoadContext et celui qui utilise des domaines d’application.There's one noteworthy difference between the unloading using AssemblyLoadContext and using AppDomains. Avec les domaines d’application, le déchargement est forcé.With AppDomains, the unloading is forced. Au moment du déchargement, tous les threads qui s’exécutent dans l’AppDomain cible sont abandonnés, les objets COM managés créés dans l’AppDomain cible sont détruits, et ainsi de suite.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. Avec AssemblyLoadContext, le déchargement est « coopératif ».With AssemblyLoadContext, the unload is "cooperative". L’appel de la méthode AssemblyLoadContext.Unload lance simplement le déchargement.Calling the AssemblyLoadContext.Unload method just initiates the unloading. Le déchargement se termine quand :The unloading finishes after:

  • Aucun thread n’a de méthodes provenant des assemblys chargés dans AssemblyLoadContext sur leurs piles d’appels.No threads have methods from the assemblies loaded into the AssemblyLoadContext on their call stacks.
  • Aucun des types des assemblys chargés dans le, les AssemblyLoadContext instances de ces types et les assemblys eux-mêmes sont référencés par :None of the types from the assemblies loaded into the AssemblyLoadContext, instances of those types, and the assemblies themselves are referenced by:

Utiliser l’AssemblyLoadContext pouvant être collectéUse collectible AssemblyLoadContext

Cette section contient un tutoriel pas à pas qui présente un moyen simple de charger une application .NET Core dans un AssemblyLoadContext pouvant être collecté, d’exécuter son point d’entrée, puis de procéder au déchargement.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. Vous trouverez un exemple complet à l’adresse 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.

Créer un AssemblyLoadContext pouvant être collectéCreate a collectible AssemblyLoadContext

Vous devez dériver votre classe de AssemblyLoadContext et substituer sa AssemblyLoadContext.Load méthode.You need to derive your class from the AssemblyLoadContext and override its AssemblyLoadContext.Load method. Cette méthode résout les références à tous les assemblys qui sont des dépendances d’assemblys chargés dans cet AssemblyLoadContext.That method resolves references to all assemblies that are dependencies of assemblies loaded into that AssemblyLoadContext.

Le code suivant est un exemple d’un AssemblyLoadContext de base personnalisé :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;
    }
}

Comme vous pouvez le voir, la méthode Load retourne null.As you can see, the Load method returns null. Cela signifie que tous les assemblys de dépendances sont chargés dans le contexte par défaut, et que le nouveau contexte contient uniquement les assemblys explicitement chargés dans celui-ci.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.

Si vous souhaitez également charger une partie ou la totalité des dépendances dans AssemblyLoadContext, vous pouvez utiliser AssemblyDependencyResolver dans la méthode 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. AssemblyDependencyResolverRésout les noms d’assembly en chemins d’accès absolus au fichier d’assembly.The AssemblyDependencyResolver resolves the assembly names to absolute assembly file paths. Le programme de résolution utilise la .deps.jssur les fichiers d’assembly et de fichier dans le répertoire de l’assembly principal chargé dans le contexte.The resolver uses the .deps.json file and assembly files in the directory of the main assembly loaded into the context.

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

Utiliser un AssemblyLoadContext personnalisé pouvant être collectéUse a custom collectible AssemblyLoadContext

Cette section part du principe que vous utilisez la version de base de TestAssemblyLoadContext.This section assumes the simpler version of the TestAssemblyLoadContext is being used.

Pour créer une instance d’un AssemblyLoadContext personnalisé et y charger un assembly, effectuez les étapes suivantes :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);

Pour chacun des assemblys référencés par l’assembly chargé, la méthode TestAssemblyLoadContext.Load est appelée afin que TestAssemblyLoadContext puisse déterminer où obtenir 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. Dans notre cas, elle retourne null pour indiquer qu’il doit être chargé dans le contexte par défaut à partir d’emplacements que le runtime utilise pour charger les assemblys par défaut.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.

Maintenant qu’un assembly a été chargé, vous pouvez exécuter une méthode à partir de celui-ci.Now that an assembly was loaded, you can execute a method from it. Exécutez la méthode Main :Run the Main method:

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

Quand la méthode Main retourne une valeur, vous pouvez lancer le déchargement en appelant la méthode Unload sur l’AssemblyLoadContext personnalisé ou en éliminant la référence à 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();

Cela suffit pour décharger l’assembly de test.This is sufficient to unload the test assembly. Nous allons à présent placer tout cela dans une méthode distincte non inlineable de telle sorte que TestAssemblyLoadContext, Assembly et MethodInfo (Assembly.EntryPoint) ne puissent pas être maintenus actifs par des références d’emplacement de pile (variables locales en temps réel ou juste-à-temps).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). En effet, le déchargement n’a pas lieu si TestAssemblyLoadContext reste actif.That could keep the TestAssemblyLoadContext alive and prevent the unload.

Retournez également une référence faible à AssemblyLoadContext afin que vous puissiez l’utiliser par la suite pour détecter la fin du déchargement.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;
}

Vous pouvez maintenant exécuter cette fonction pour charger, exécuter et décharger 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);

Toutefois, le déchargement ne se termine pas immédiatement.However, the unload doesn't complete immediately. Comme mentionné précédemment, il s’appuie sur le garbage collector pour collecter tous les objets de l’assembly de test.As previously mentioned, it relies on the garbage collector to collect all the objects from the test assembly. Dans de nombreux cas, il n’est pas nécessaire d’attendre la fin du déchargement.In many cases, it isn't necessary to wait for the unload completion. Toutefois, dans certains cas, il est utile de savoir que le déchargement est terminé.However, there are cases where it's useful to know that the unload has finished. Par exemple, vous souhaitez peut-être supprimer le fichier d’assembly qui a été chargé dans l’AssemblyLoadContext personnalisé du disque.For example, you may want to delete the assembly file that was loaded into the custom AssemblyLoadContext from disk. Dans ce cas, vous pouvez utiliser l’extrait de code suivant.In such a case, the following code snippet can be used. Il déclenche garbage collection et attend les finaliseurs en attente dans une boucle jusqu’à ce que la référence faible à la personnalisée AssemblyLoadContext soit définie sur null , ce qui indique que l’objet cible a été collecté.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. Dans la plupart des cas, un seul passage à la boucle est requis.In most cases, just one pass through the loop is required. Toutefois, dans les cas plus complexes où les objets créés par le code qui s’exécute dans AssemblyLoadContext ont des finaliseurs, des passages supplémentaires peuvent être nécessaires.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();
}

Événement de déchargementThe Unloading event

Dans certains cas, le code chargé dans un AssemblyLoadContext personnalisé doit effectuer des tâches de nettoyage au moment du lancement du déchargement.In some cases, it may be necessary for the code loaded into a custom AssemblyLoadContext to perform some cleanup when the unloading is initiated. Par exemple, il peut être nécessaire d’arrêter des threads ou de nettoyer des handles de GC forts.For example, it may need to stop threads or clean up strong GC handles. L’événement Unloading peut être utilisé dans de tels cas.The Unloading event can be used in such cases. Un gestionnaire qui effectue le nettoyage nécessaire peut être raccroché à cet événement.A handler that performs the necessary cleanup can be hooked to this event.

Résoudre les problèmes de déchargementTroubleshoot unloadability issues

En raison de la nature coopérative du déchargement, il est facile d’oublier les références qui peuvent conserver le contenu d’un collecté AssemblyLoadContext actif et empêcher le déchargement.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. Voici un résumé des entités (certaines d’entre elles non évidentes) qui peuvent contenir les références :Here is a summary of entities (some of them non-obvious) that can hold the references:

  • Les références régulières détenues de l’extérieur de la collection AssemblyLoadContext sont stockées dans un emplacement de pile ou un registre de processeur (variables locales, créées explicitement par le code utilisateur ou implicitement par le compilateur juste-à-temps (JIT)), une variable statique ou un handle de GC fort (épinglage) et pointant vers :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 chargé dans l’AssemblyLoadContext pouvant être collecté.An assembly loaded into the collectible AssemblyLoadContext.
    • Un type d’un tel assembly.A type from such an assembly.
    • Une instance d’un type à partir d’un tel assembly.An instance of a type from such an assembly.
  • Threads exécutant du code à partir d’un assembly chargé dans l’AssemblyLoadContext pouvant être collecté.Threads running code from an assembly loaded into the collectible AssemblyLoadContext.
  • Instances de types personnalisés et non pouvant être collectés AssemblyLoadContext créés à l’intérieur de l’objets pouvant être collectés AssemblyLoadContext .Instances of custom, non-collectible AssemblyLoadContext types created inside of the collectible AssemblyLoadContext.
  • RegisteredWaitHandleLes instances en attente avec des rappels sont définies sur les méthodes dans le personnalisé AssemblyLoadContext .Pending RegisteredWaitHandle instances with callbacks set to methods in the custom AssemblyLoadContext.

Conseil

Les références d’objets stockées dans des emplacements de pile ou des registres de processeur et qui peuvent empêcher le déchargement d’un AssemblyLoadContext peuvent se produire dans les situations suivantes :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:

  • Lorsque les résultats de l’appel de fonction sont passés directement à une autre fonction, même s’il n’y a aucune variable locale créée par l’utilisateur.When function call results are passed directly to another function, even though there is no user-created local variable.
  • Quand le compilateur JIT conserve une référence à un objet qui était disponible à un moment donné dans une méthode.When the JIT compiler keeps a reference to an object that was available at some point in a method.

Déboguer les problèmes de déchargementDebug unloading issues

Le débogage des problèmes de déchargement peut être fastidieux.Debugging issues with unloading can be tedious. Vous pouvez vous retrouver dans des situations où le déchargement échoue sans que vous sachiez ce qui maintient un AssemblyLoadContext actif.You can get into situations where you don't know what can be holding an AssemblyLoadContext alive, but the unload fails. Le meilleur moyen de résoudre ce genre de problème est d’utiliser WinDbg (LLDB sur UNIX) avec le plug-in SOS.The best weapon to help with that is WinDbg (LLDB on Unix) with the SOS plugin. Vous devez déterminer ce qui fait qu’un LoaderAllocator appartenant à l’AssemblyLoadContext donné reste actif.You need to find what's keeping a LoaderAllocator belonging to the specific AssemblyLoadContext alive. Le plug-in SOS vous permet d’examiner les objets du tas GC, leurs hiérarchies et leurs racines.The SOS plugin allows you to look at GC heap objects, their hierarchies, and roots.

Pour charger le plug-in dans le débogueur, entrez la commande suivante dans la ligne de commande du débogueur :To load the plugin into the debugger, enter the following command in the debugger command line:

Dans WinDbg (apparemment, WinDbg le fait automatiquement quand il s’arrête dans une application .NET Core) :In WinDbg (it seems WinDbg does that automatically when breaking into .NET Core application):

.loadby sos coreclr

Dans LLDB :In LLDB:

plugin load /path/to/libsosplugin.so

Déboguez un exemple de programme qui rencontre des problèmes de déchargement.Let's debug an example program that has problems with unloading. Le code source est inclus ci-dessous.Source code is included below. Quand vous l’exécutez sous WinDbg, le programme s’arrête dans le débogueur juste après la tentative de vérification de la réussite du déchargement.When you run it under WinDbg, the program breaks into the debugger right after attempting to check for the unload success. Vous pouvez ensuite commencer à rechercher les coupables.You can then start looking for the culprits.

Conseil

Si vous déboguez à l’aide de LLDB sur UNIX, les commandes SOS dans les exemples suivants n’ont pas le ! devant.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

Cette commande vide tous les objets dont le nom de type contient LoaderAllocator et qui se trouvent dans le tas GC.This command dumps all objects with a type name containing LoaderAllocator that are in the GC heap. Voici un exemple :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

Dans la partie « Statistics: » ci-dessous, examinez l’élément MT (MethodTable) appartenant à System.Reflection.LoaderAllocator, qui est l’objet qui nous intéresse.In the "Statistics:" part below, check the MT (MethodTable) belonging to the System.Reflection.LoaderAllocator, which is the object we care about. Ensuite, dans la liste au début, recherchez l’entrée correspondant à celle MT -ci et obtenez l’adresse de l’objet lui-même.Then, in the list at the beginning, find the entry with MT matching that one and get the address of the object itself. Dans notre cas, il s’agit de « 000002b78000ce40 ».In our case, it is "000002b78000ce40".

Maintenant que nous savons l’adresse de l' LoaderAllocator objet, nous pouvons utiliser une autre commande pour rechercher ses racines GC :Now that we know the address of the LoaderAllocator object, we can use another command to find its GC roots:

!gcroot -all 0x000002b78000ce40

Cette commande vide la chaîne des références d’objet qui mènent à l’instance LoaderAllocator.This command dumps the chain of object references that lead to the LoaderAllocator instance. La liste commence par la racine, qui est l’entité qui conserve notre LoaderAllocator état actif et constitue donc le cœur du problème.The list starts with the root, which is the entity that keeps our LoaderAllocator alive and thus is the core of the problem. La racine peut être un emplacement de pile, un registre de processeur, un handle de GC ou une variable statique.The root can be a stack slot, a processor register, a GC handle, or a static variable.

Voici un exemple de sortie de la commande 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.

L’étape suivante consiste à déterminer où se trouve la racine afin de pouvoir la corriger.The next step is to figure out where the root is located so you can fix it. Dans le cas le plus simple, la racine est un emplacement de pile ou un registre de processeur.The easiest case is when the root is a stack slot or a processor register. Dans ce cas, le gcroot affiche le nom de la fonction dont le frame contient la racine et le thread qui exécute cette fonction.In that case, the gcroot shows the name of the function whose frame contains the root and the thread executing that function. La situation est plus complexe quand la racine est une variable statique ou un handle de GC.The difficult case is when the root is a static variable or a GC handle.

Dans l’exemple précédent, la première racine est un local de type System.Reflection.RuntimeMethodInfo stocké dans le frame de la fonction example.Program.Main(System.String[]) à l’adresse rbp-20 (rbp est le registre de processeur rbp et -20 est un décalage hexadécimal de ce registre).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 deuxième racine est un GCHandle normal (fort) qui contient une référence à une instance de la classe test.Test.The second root is a normal (strong) GCHandle that holds a reference to an instance of the test.Test class.

La troisième racine est un GCHandle épinglé.The third root is a pinned GCHandle. Il s’agit en fait d’une variable statique, mais malheureusement, il n’existe aucun moyen de le savoir.This one is actually a static variable, but unfortunately, there is no way to tell. Les variables statiques pour les types références sont stockées dans un tableau d’objets managés dans des structures de runtime internes.Statics for reference types are stored in a managed object array in internal runtime structures.

Le déchargement d’un AssemblyLoadContext peut aussi ne pas aboutir quand un thread a le frame d’une méthode d’un assembly chargé dans l’AssemblyLoadContext sur sa pile.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. Pour vérifier cela, videz les piles d’appels managées de tous les threads :You can check that by dumping managed call stacks of all threads:

~*e !clrstack

La commande signifie « appliquer à tous les threads la commande !clrstack ».The command means "apply to all threads the !clrstack command". Voici la sortie de cette commande pour l’exemple.The following is the output of that command for the example. Malheureusement, LLDB sur UNIX n’a aucun moyen d’appliquer une commande à tous les threads. vous devez donc basculer manuellement les threads et répéter la clrstack commande.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. Ignore tous les threads où le débogueur indique « Impossible de parcourir la pile managée ».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]

Comme vous pouvez le voir, le dernier thread a test.Program.ThreadProc().As you can see, the last thread has test.Program.ThreadProc(). Il s’agit d’une fonction de l’assembly chargé dans AssemblyLoadContext qui maintient AssemblyLoadContext actif.This is a function from the assembly loaded into the AssemblyLoadContext, and so it keeps the AssemblyLoadContext alive.

Exemple de source avec des problèmes de déchargementExample source with unloadability issues

Le code suivant est utilisé dans l’exemple de débogage précédent.The following code is used in the previous debugging example.

Programme de test principalMain 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}");
        }
    }
}

Programme chargé dans TestAssemblyLoadContextProgram loaded into the TestAssemblyLoadContext

Le code suivant représente l' test.dll passé à la ExecuteAndUnload méthode dans le programme de test principal.The following code represents the test.dll passed to the ExecuteAndUnload method in the main testing program.

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

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

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