Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
.NET (Core) introdujo la capacidad de cargar y descargar posteriormente un conjunto de ensamblados. En .NET Framework, los dominios de aplicación personalizados se usaron para este propósito, pero .NET (Core) solo admite un único dominio de aplicación predeterminado.
La descarga se admite a través de AssemblyLoadContext. Puede cargar un conjunto de ensamblados en un elemento recopilable AssemblyLoadContext, ejecutar métodos en ellos o simplemente inspeccionarlos mediante el uso de reflection, y, por último, descargarlos del AssemblyLoadContext. Que descarga los ensamblados cargados en el AssemblyLoadContext.
Hay una diferencia notable entre la descarga mediante AssemblyLoadContext y el uso de AppDomains. Con AppDomains, se fuerza la descarga. En el momento de la descarga, se anulan todos los subprocesos que se ejecutan en el AppDomain de destino, se destruyen los objetos COM administrados creados en el AppDomain de destino, y se realizan otras operaciones similares. Con AssemblyLoadContext, la descarga es "cooperativa". Llamar al AssemblyLoadContext.Unload método simplemente inicia la descarga. La descarga finaliza después de:
- Ningún subproceso tiene métodos de los ensamblados cargados en
AssemblyLoadContexten su pila de llamadas. - Ninguno de los tipos de los ensamblados cargados en
AssemblyLoadContext, las instancias de esos tipos ni los propios ensamblados son referenciados por:- Referencias fuera de
AssemblyLoadContext, excepto las referencias débiles (WeakReference o WeakReference<T>). - Manejo fuerte por el recolector de basura (GCHandleType.Normal o GCHandleType.Pinned) tanto desde dentro como desde fuera de
AssemblyLoadContext.
- Referencias fuera de
Uso de AssemblyLoadContext coleccionable
Esta sección contiene un tutorial detallado paso a paso que muestra una manera sencilla de cargar una aplicación de .NET (Core) en un AssemblyLoadContext recopilable, ejecutar su punto de entrada y luego descargarla. Puede encontrar un ejemplo completo en https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Creación de un AssemblyLoadContext recopilable
Derive la clase de AssemblyLoadContext e invalide su AssemblyLoadContext.Load método. Ese método resuelve las referencias a todos los ensamblados que dependen de los ensamblados cargados en ese AssemblyLoadContext.
El código siguiente es un ejemplo del personalizado AssemblyLoadContextmás sencillo:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Como puede ver, el Load método devuelve null. Esto significa que todos los ensamblados de dependencia se cargan en el contexto predeterminado y el nuevo contexto solo contiene los ensamblados cargados explícitamente en él.
Si también desea cargar algunas o todas las dependencias en AssemblyLoadContext, puede usar el AssemblyDependencyResolver en el método Load.
AssemblyDependencyResolver resuelve los nombres de ensamblado en rutas de acceso absolutas de archivos de ensamblaje. El solucionador usa el archivo .deps.json y los archivos de ensamblado en el directorio del ensamblado principal cargado en el contexto.
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;
}
}
}
Usar un AssemblyLoadContext coleccionable personalizado
En esta sección se supone que se usa la versión más sencilla de .TestAssemblyLoadContext
Puede crear una instancia del objeto customizado AssemblyLoadContext y cargar un ensamblado en ésta de la siguiente manera:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Para cada uno de los ensamblados a los que hace referencia el ensamblado cargado, se llama al TestAssemblyLoadContext.Load método para que TestAssemblyLoadContext pueda decidir de dónde obtener el ensamblado. En este caso, devuelve null para indicar que se debe cargar en el contexto predeterminado desde ubicaciones que el runtime usa para cargar ensamblados de forma predeterminada.
Ahora que se cargó un ensamblado, puede ejecutar un método desde él. Ejecute el Main método :
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Una vez que el método Main ha devuelto, puede iniciar la descarga llamando al método Unload en el AssemblyLoadContext personalizado o quitando la referencia que tiene a AssemblyLoadContext.
alc.Unload();
Esto es suficiente para descargar el ensamblado de prueba. A continuación, colocará todo esto en un método que no puede ser insertado para asegurarse de que los TestAssemblyLoadContext, Assembly, y MethodInfo (los Assembly.EntryPoint) no puedan permanecer activos por referencias de posiciones en la pila introducidas por el JIT o reales. Eso podría mantener el TestAssemblyLoadContext activo y evitar la descarga.
Además, devuelva una referencia débil a AssemblyLoadContext para que pueda usarla más adelante para detectar la finalización de la descarga.
[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
alcWeakRef = new WeakReference(alc, trackResurrection: true);
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
alc.Unload();
}
Ahora puede ejecutar esta función para cargar, ejecutar y descargar el ensamblado.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Sin embargo, la descarga no se completa inmediatamente. Como se mencionó anteriormente, se basa en el recolector de basura para recoger todos los objetos del ensamblado de prueba. En muchos casos, no es necesario esperar a la finalización de la descarga. Sin embargo, hay casos en los que resulta útil saber que la descarga ha finalizado. Por ejemplo, es posible que desee eliminar el archivo de ensamblado que se cargó en el disco personalizado AssemblyLoadContext . En tal caso, se puede usar el siguiente fragmento de código. Inicia la recolección de elementos no utilizados y espera los finalizadores pendientes en un bucle hasta que la referencia débil al AssemblyLoadContext personalizado se establezca en null, lo que indica que el objeto de destino fue recolectado. En la mayoría de los casos, solo se requiere un paso por el bucle. Sin embargo, en los casos más complejos en los que los objetos creados por el código que se ejecuta en el AssemblyLoadContext tienen finalizadores, es posible que se necesiten más pases.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Limitaciones
Los ensamblados cargados en un AssemblyLoadContext recopilable deben cumplir las restricciones generales de los ensamblados recopilables. Además, se aplican las siguientes limitaciones:
- No se admiten ensamblados escritos en C++/CLI.
- Se omitirá el código generado ReadyToRun.
Evento de descarga
En algunos casos, podría ser necesario que el código cargado en un personalizado AssemblyLoadContext realice alguna limpieza cuando se inicie la descarga. Por ejemplo, puede que tenga que detener los subprocesos o limpiar los identificadores de GC fuertes. El Unloading evento se puede usar en tales casos. Puede enlazar un controlador que realice la limpieza necesaria para este evento.
Solución de problemas de imposibilidad de carga
Debido a la naturaleza cooperativa de la descarga, es fácil olvidarse de las referencias que podrían estar manteniendo los elementos vivos en una estructura coleccionable AssemblyLoadContext y previniendo la descarga. Este es un resumen de las entidades (algunas de ellas noobviosas) que pueden contener las referencias:
- Referencias regulares que se mantienen fuera de la colección
AssemblyLoadContextque se almacenan en una ranura de pila o en un registro de procesador (métodos locales, ya sea creados explícitamente por el código de usuario o implícitamente por el compilador Just-In-Time (JIT), una variable estática o un identificador gc seguro (anclado) y apuntando transitivamente a:- Un ensamblaje cargado en el objeto collectible
AssemblyLoadContext. - Tipo de un ensamblaje así.
- Una instancia de un tipo de dicho ensamblado.
- Un ensamblaje cargado en el objeto collectible
- Subprocesos que ejecutan código desde un ensamblado cargado en un recolector.
AssemblyLoadContext - Instancias de tipos
AssemblyLoadContextpersonalizados y no recopilables creadas dentro del coleccionableAssemblyLoadContext. - Instancias pendientes RegisteredWaitHandle con devoluciones de llamada establecidas en métodos del
AssemblyLoadContextpersonalizado. - Campos de la subclase personalizada
AssemblyLoadContextque hacen referencia a ensamblados, tipos o instancias de tipos cargados en elAssemblyLoadContextcolector. Mientras la descarga está en curso, el entorno de ejecución mantiene un identificador GC sólido para coordinar la descargaAssemblyLoadContext. Esto significa que el GC no recopilará esas referencias de campo incluso después de quitar su propia referencia a .AssemblyLoadContextBorre estos campos para que la descarga se pueda completar.
Sugerencia
Las referencias de objeto almacenadas en ranuras de pila o registros de procesador y que podrían impedir la descarga de un AssemblyLoadContext objeto pueden producirse en las situaciones siguientes:
- Cuando los resultados de la llamada de función se pasan directamente a otra función, aunque no haya ninguna variable local creada por el usuario.
- Cuando el compilador JIT mantiene una referencia a un objeto que estaba disponible en algún momento de un método.
Problemas al depurar el proceso de descarga
Los problemas de depuración durante el proceso de descarga pueden ser tediosos. Puede entrar en situaciones en las que no sepa lo que puede estar manteniendo AssemblyLoadContext vivo, pero se produce un error en la descarga. La mejor herramienta para ayudar con esto es WinDbg (o LLDB en Unix) con el complemento SOS. Debe encontrar lo que mantiene activo un LoaderAllocator elemento que pertenece a la instancia AssemblyLoadContext específica. El complemento SOS le permite examinar los objetos del montón de GC, sus jerarquías y raíces.
Para cargar el complemento SOS en el depurador, escriba uno de los siguientes comandos en la línea de comandos del depurador.
En WinDbg (si aún no está cargado):
.loadby sos coreclr
En LLDB:
plugin load /path/to/libsosplugin.so
Ahora depurará un programa de ejemplo que tiene problemas con la descarga. El código fuente está disponible en la sección Código fuente de ejemplo . Al ejecutarlo en WinDbg, el programa se interrumpe en el depurador justo después de intentar verificar el éxito de la descarga. A continuación, puedes empezar a buscar a los culpables.
Sugerencia
Si depura con LLDB en Unix, los comandos SOS de los ejemplos siguientes no tienen ! delante.
!dumpheap -type LoaderAllocator
Este comando vuelca todos los objetos con un nombre de tipo que contiene LoaderAllocator en el montón de GC. Este es un ejemplo:
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
En la parte "Estadísticas:", compruebe el MT (MethodTable) que pertenece a System.Reflection.LoaderAllocator, que es el objeto que le interesa. A continuación, en la lista al principio, busque la entrada que coincida con MT y obtenga la dirección del propio objeto. En este caso, es "000002b78000ce40".
Ahora que conoce la dirección del LoaderAllocator objeto, puede usar otro comando para buscar sus raíces de GC:
!gcroot 0x000002b78000ce40
Este comando volca la cadena de referencias de objeto que conducen a la LoaderAllocator instancia. La lista comienza con la raíz, que es la entidad que mantiene activa LoaderAllocator y, por tanto, es el núcleo del problema. La raíz puede ser una ranura de pila, un registro de procesador, un identificador GC o una variable estática.
Este es un ejemplo de la salida del gcroot comando:
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.
El siguiente paso consiste en averiguar dónde se encuentra la raíz para que pueda corregirla. El caso más sencillo es cuando la raíz es una ranura de pila o un registro de procesador. En ese caso, gcroot muestra el nombre de la función cuyo marco contiene la raíz y el subproceso que ejecuta esa función. El caso difícil es cuando la raíz es una variable estática o un control de GC.
En el ejemplo anterior, la primera raíz es un local de tipo System.Reflection.RuntimeMethodInfo almacenado en el marco de la función example.Program.Main(System.String[]) en la dirección rbp-20 (rbp es el registro rbp del procesador y -20 es un desplazamiento hexadecimal de ese registro).
La segunda raíz es una normal (fuerte) GCHandle que contiene una referencia a una instancia de la test.Test clase .
La tercera raíz es una fijada GCHandle. Esta es realmente una variable estática, pero desafortunadamente, no hay forma de decir. Las estáticas de los tipos de referencia se almacenan en una matriz de objetos administrados en estructuras internas durante el tiempo de ejecución.
Otro caso que puede impedir la descarga de un AssemblyLoadContext es cuando un subproceso tiene un marco de un método de un ensamblado cargado en en AssemblyLoadContext su pila. Puede comprobarlo al volcar pilas de llamadas administradas de todos los subprocesos:
~*e !clrstack
El comando significa "aplicar a todos los subprocesos el !clrstack comando". A continuación se muestra la salida de ese comando para el ejemplo. Desafortunadamente, LLDB en Unix no tiene ninguna manera de aplicar un comando a todos los subprocesos, por lo que debe cambiar manualmente los subprocesos y repetir el clrstack comando. Ignora todos los hilos donde el depurador dice "No se puede recorrer la pila administrada".
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]
Como puedes ver, el último hilo tiene test.Program.ThreadProc(). Se trata de una función del ensamblaje cargado en AssemblyLoadContext, y por tanto, mantiene vivo al AssemblyLoadContext.
Código fuente de ejemplo
El siguiente código que contiene problemas de descargabilidad se utiliza en el ejemplo de depuración anterior.
Programa de pruebas principales
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace example
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
class TestInfo
{
public TestInfo(MethodInfo? mi)
{
_entryPoint = mi;
}
MethodInfo? _entryPoint;
}
class Program
{
static TestInfo? entryPoint;
[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
{
var alc = new TestAssemblyLoadContext();
testAlcWeakRef = new WeakReference(alc);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
if (a == null)
{
testEntryPoint = null;
Console.WriteLine("Loading the test assembly failed");
return -1;
}
var args = new object[1] {new string[] {"Hello"}};
// Issue preventing unloading #1 - we keep MethodInfo of a method
// for an assembly loaded into the TestAssemblyLoadContext in a static variable.
entryPoint = new TestInfo(a.EntryPoint);
testEntryPoint = a.EntryPoint;
var oResult = a.EntryPoint?.Invoke(null, args);
alc.Unload();
return (oResult is int result) ? result : -1;
}
static void Main(string[] args)
{
WeakReference testAlcWeakRef;
// Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
MethodInfo? testEntryPoint;
int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
System.Diagnostics.Debugger.Break();
Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
}
}
}
Programa cargado en TestAssemblyLoadContext
El código siguiente representa el test.dll pasado al ExecuteAndUnload método en el programa de pruebas principal.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace test
{
class Test
{
}
class Program
{
public static void ThreadProc()
{
// Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
Thread.Sleep(Timeout.Infinite);
}
static GCHandle handle;
static int Main(string[] args)
{
// Issue preventing unloading #3 - normal GC handle
handle = GCHandle.Alloc(new Test());
Thread t = new Thread(new ThreadStart(ThreadProc));
t.IsBackground = true;
t.Start();
Console.WriteLine($"Hello from the test: args[0] = {args[0]}");
return 1;
}
}
}