De laadbaarheid van assembly's gebruiken en fouten opsporen in .NET
.NET (Core) heeft de mogelijkheid geïntroduceerd om een set assembly's te laden en later te verwijderen. In .NET Framework zijn hiervoor aangepaste app-domeinen gebruikt, maar .NET (Core) ondersteunt slechts één standaard-app-domein.
De laadbaarheid wordt ondersteund via AssemblyLoadContext. U kunt een set assembly's in een verzamelbare AssemblyLoadContext
verzameling laden, methoden erin uitvoeren of ze gewoon inspecteren met weerspiegeling en ten slotte de AssemblyLoadContext
. Hiermee worden de assembly's die in het bestand zijn geladen, uitgepakt AssemblyLoadContext
.
Er is één opmerkelijk verschil tussen het lossen met behulp van AssemblyLoadContext
en het gebruik van AppDomains. Met AppDomains wordt het lossen gedwongen. Bij het uitladen worden alle threads die in het doel-AppDomain worden uitgevoerd, afgebroken, beheerde COM-objecten die in het doel-AppDomain zijn gemaakt, vernietigd, enzovoort. Met AssemblyLoadContext
, de ontlading is "coöperatie". Als u de methode aanroept, wordt het AssemblyLoadContext.Unload lossen gestart. Het lossen is voltooid na:
- Er zijn geen threads die methoden hebben van de assembly's die in de
AssemblyLoadContext
aanroepstacks zijn geladen. - Geen van de typen van de assembly's die in de
AssemblyLoadContext
assembly's zijn geladen, exemplaren van deze typen en de assembly's zelf worden verwezen door:- Verwijzingen buiten de
AssemblyLoadContext
, met uitzondering van zwakke verwijzingen (WeakReference of WeakReference<T>). - Sterke garbagecollection (GC) ingangen (GCHandleType.Normal of GCHandleType.Pinned) van zowel binnen als buiten de
AssemblyLoadContext
.
- Verwijzingen buiten de
Collectible AssemblyLoadContext gebruiken
Deze sectie bevat een gedetailleerde stapsgewijze zelfstudie waarin een eenvoudige manier wordt getoond om een .NET-toepassing (Core) in een verzamelbare AssemblyLoadContext
toepassing te laden, het ingangspunt uit te voeren en het vervolgens te verwijderen. U vindt een volledig voorbeeld op https://github.com/dotnet/samples/tree/main/core/tutorials/Unloading.
Een collectible AssemblyLoadContext maken
Uw klas afleiden van de AssemblyLoadContext methode en deze overschrijven AssemblyLoadContext.Load . Deze methode lost verwijzingen op naar alle assembly's die afhankelijkheden zijn van assembly's die in dat AssemblyLoadContext
bestand zijn geladen.
De volgende code is een voorbeeld van de eenvoudigste aangepaste AssemblyLoadContext
code:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Zoals u ziet, retourneert null
de Load
methode . Dit betekent dat alle afhankelijkheidsassembly's in de standaardcontext worden geladen en dat de nieuwe context alleen de assembly's bevat die expliciet in de assembly's zijn geladen.
Als u bepaalde of alle afhankelijkheden in de AssemblyLoadContext
app wilt laden, kunt u de AssemblyDependencyResolver
Load
methode gebruiken. Hiermee AssemblyDependencyResolver
worden de assemblynamen omgezet in absolute assemblybestandspaden. De resolver gebruikt de .deps.json bestands- en assemblybestanden in de map van de hoofdassembly die in de context is geladen.
using System.Reflection;
using System.Runtime.Loader;
namespace complex
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public TestAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
}
protected override Assembly? Load(AssemblyName name)
{
string? assemblyPath = _resolver.ResolveAssemblyToPath(name);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
}
}
Een aangepaste collectible AssemblyLoadContext gebruiken
In deze sectie wordt ervan uitgegaan dat de eenvoudigere versie van de TestAssemblyLoadContext
versie wordt gebruikt.
U kunt als volgt een exemplaar van de aangepaste AssemblyLoadContext
maken en een assembly erin laden:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Voor elk van de assembly's waarnaar wordt verwezen door de geladen assembly, wordt de TestAssemblyLoadContext.Load
methode aangeroepen zodat de TestAssemblyLoadContext
assembly kan bepalen waar de assembly vandaan komt. In dit geval wordt geretourneerd null
om aan te geven dat deze moet worden geladen in de standaardcontext vanaf locaties die door de runtime worden gebruikt om standaard assembly's te laden.
Nu een assembly is geladen, kunt u er een methode van uitvoeren. Voer de Main
methode uit:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
Nadat de Main
methode is geretourneerd, kunt u het lossen initiëren door de Unload
methode aan te roepen op de aangepaste AssemblyLoadContext
methode of door de verwijzing naar het AssemblyLoadContext
volgende te verwijderen:
alc.Unload();
Dit is voldoende om de testassembly te ontladen. Vervolgens plaatst u dit allemaal in een afzonderlijke niet-inlineerbare methode om ervoor te zorgen dat de TestAssemblyLoadContext
, Assembly
en MethodInfo
(de Assembly.EntryPoint
) niet kunnen worden bewaard door stack-siteverwijzingen (echte of door JIT geïntroduceerde lokale bevolking). Dat kan het TestAssemblyLoadContext
leven behouden en het ontladen voorkomen.
Retourneer ook een zwakke verwijzing naar de AssemblyLoadContext
zodat u deze later kunt gebruiken om de voltooiing van het laden te detecteren.
[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
alcWeakRef = new WeakReference(alc, trackResurrection: true);
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
alc.Unload();
}
U kunt deze functie nu uitvoeren om de assembly te laden, uit te voeren en te ontladen.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Het uitladen wordt echter niet onmiddellijk voltooid. Zoals eerder vermeld, is het afhankelijk van de garbagecollector om alle objecten van de testassembly te verzamelen. In veel gevallen is het niet nodig om te wachten totdat het laden is voltooid. Er zijn echter gevallen waarin het handig is om te weten dat het verwijderen is voltooid. U kunt bijvoorbeeld het assemblybestand verwijderen dat is geladen in de aangepaste AssemblyLoadContext
schijf. In dat geval kan het volgende codefragment worden gebruikt. Het activeert garbagecollection en wacht op in behandeling zijnde finalizers in een lus totdat de zwakke verwijzing naar de aangepaste AssemblyLoadContext
is ingesteld null
op , waarmee wordt aangegeven dat het doelobject is verzameld. In de meeste gevallen is slechts één pass through de lus vereist. Voor complexere gevallen waarin objecten die zijn gemaakt door de code die wordt uitgevoerd in de AssemblyLoadContext
have finalizers, zijn er echter mogelijk meer passen nodig.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Beperkingen
Assembly's die in een verzamelobject AssemblyLoadContext
zijn geladen, moeten voldoen aan de algemene beperkingen voor verzamelbare assembly's. De volgende beperkingen zijn ook van toepassing:
- Assembly's die zijn geschreven in C++/CLI worden niet ondersteund.
- ReadyToRun gegenereerde code wordt genegeerd.
Het losmaken van het evenement
In sommige gevallen kan het nodig zijn voor de code die in een aangepaste AssemblyLoadContext
code is geladen om een opschoonbewerking uit te voeren wanneer het lossen wordt gestart. Het kan bijvoorbeeld nodig zijn om threads te stoppen of sterke GC-ingangen op te schonen. De Unloading
gebeurtenis kan in dergelijke gevallen worden gebruikt. U kunt een handler koppelen die de benodigde opschoonactie voor deze gebeurtenis uitvoert.
Problemen met uitlaadbaarheid oplossen
Vanwege de coöperatieve aard van het lossen is het gemakkelijk om verwijzingen te vergeten die het spul in een verzamelbaar AssemblyLoadContext
leven kunnen houden en het ontladen verhinderen. Hier volgt een samenvatting van entiteiten (sommige hiervan niet-obvious) die de verwijzingen kunnen bevatten:
- Reguliere verwijzingen van buiten het verzamelobject
AssemblyLoadContext
die zijn opgeslagen in een stacksite of een processorregister (methode locals, expliciet gemaakt door de gebruikerscode of impliciet door de Just-In-Time-compiler (JIT) compiler, een statische variabele of een sterke (pinning) GC-ingang, en transitief verwijst naar:- Een assembly die in het verzamelobject
AssemblyLoadContext
is geladen. - Een type van een dergelijke assembly.
- Een exemplaar van een type van een dergelijke assembly.
- Een assembly die in het verzamelobject
- Threads die code uitvoeren vanuit een assembly die in het verzamelobject
AssemblyLoadContext
is geladen. - Exemplaren van aangepaste, niet-verzamelbare
AssemblyLoadContext
typen die zijn gemaakt in de verzamelbareAssemblyLoadContext
. - In behandeling zijnde RegisteredWaitHandle exemplaren met callbacks ingesteld op methoden in de aangepaste
AssemblyLoadContext
.
Tip
Objectverwijzingen die zijn opgeslagen in stacksleuven of processorregisters en die kunnen voorkomen dat er een AssemblyLoadContext
probleem kan worden verwijderd, kunnen zich in de volgende situaties voordoen:
- Wanneer resultaten van functieaanroep rechtstreeks worden doorgegeven aan een andere functie, ook al is er geen door de gebruiker gemaakte lokale variabele.
- Wanneer de JIT-compiler een verwijzing houdt naar een object dat op een bepaald moment in een methode beschikbaar was.
Losproblemen oplossen
Foutopsporingsproblemen bij het lossen kunnen vervelend zijn. U kunt in situaties komen waarin u niet weet wat er AssemblyLoadContext
levend kan worden vastgehouden, maar het uitladen mislukt. Het beste hulpprogramma om daarmee te helpen is WinDbg (of LLDB op Unix) met de SOS-invoegtoepassing. Je moet vinden wat een LoaderAllocator
deel uitmaakt van het specifieke AssemblyLoadContext
levend. Met de SOS-invoegtoepassing kunt u GC-heapobjecten, hun hiërarchieën en wortels bekijken.
Als u de SOS-invoegtoepassing wilt laden in het foutopsporingsprogramma, voert u een van de volgende opdrachten in de opdrachtregel voor foutopsporingsprogramma in.
In WinDbg (als deze nog niet is geladen):
.loadby sos coreclr
In LLDB:
plugin load /path/to/libsosplugin.so
U gaat nu fouten opsporen in een voorbeeldprogramma dat problemen heeft met het lossen. De broncode is beschikbaar in de sectie Voorbeeldbroncode . Wanneer u het uitvoert onder WinDbg, breekt het programma in het foutopsporingsprogramma in nadat is geprobeerd te controleren of het laden is gelukt. Vervolgens kunt u op zoek gaan naar de schuldigen.
Tip
Als u fouten opssport met behulp van LLDB op Unix, hebben de SOS-opdrachten in de volgende voorbeelden niet de !
voorzijde.
!dumpheap -type LoaderAllocator
Met deze opdracht worden alle objecten gedumpt met een typenaam LoaderAllocator
die zich in de GC-heap bevindt. Hier volgt een voorbeeld:
Address MT Size
000002b78000ce40 00007ffadc93a288 48
000002b78000ceb0 00007ffadc93a218 24
Statistics:
MT Count TotalSize Class Name
00007ffadc93a218 1 24 System.Reflection.LoaderAllocatorScout
00007ffadc93a288 1 48 System.Reflection.LoaderAllocator
Total 2 objects
Controleer in het gedeelte Statistieken:de MT
(MethodTable
) die deel uitmaakt van het System.Reflection.LoaderAllocator
object waar u om geeft. Zoek vervolgens in de lijst aan het begin de vermelding met MT
die vermelding en haal het adres van het object zelf op. In dit geval is het '000002b78000ce40'.
Nu u het adres van het LoaderAllocator
object kent, kunt u een andere opdracht gebruiken om de GC-wortels te vinden:
!gcroot 0x000002b78000ce40
Met deze opdracht wordt de keten van objectverwijzingen gedumpt die naar het LoaderAllocator
exemplaar leiden. De lijst begint met de hoofdmap, die de entiteit is die het LoaderAllocator
levend houdt en dus de kern van het probleem is. De hoofdmap kan een stacksite, een processorregister, een GC-ingang of een statische variabele zijn.
Hier volgt een voorbeeld van de uitvoer van de gcroot
opdracht:
Thread 4ac:
000000cf9499dd20 00007ffa7d0236bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
rbp-20: 000000cf9499dd90
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
HandleTable:
000002b7f8a81198 (strong handle)
-> 000002b78000d948 test.Test
-> 000002b78000ce40 System.Reflection.LoaderAllocator
000002b7f8a815f8 (pinned handle)
-> 000002b790001038 System.Object[]
-> 000002b78000d390 example.TestInfo
-> 000002b78000d328 System.Reflection.RuntimeMethodInfo
-> 000002b78000d1f8 System.RuntimeType+RuntimeTypeCache
-> 000002b78000d1d0 System.RuntimeType
-> 000002b78000ce40 System.Reflection.LoaderAllocator
Found 3 roots.
De volgende stap is om erachter te komen waar de hoofdmap zich bevindt, zodat u deze kunt oplossen. Het eenvoudigste geval is wanneer de hoofdmap een stacksite of een processorregister is. In dat geval gcroot
ziet u de naam van de functie waarvan het frame de hoofdmap en de thread bevat die die functie uitvoert. Het moeilijke geval is wanneer de hoofdmap een statische variabele of een GC-ingang is.
In het vorige voorbeeld is de eerste hoofdmap een lokaal type System.Reflection.RuntimeMethodInfo
dat is opgeslagen in het frame van de functie example.Program.Main(System.String[])
op adres rbp-20
(rbp
is het processorregister rbp
en -20 een hexadecimale offset van dat register).
De tweede hoofdmap is een normale (sterke) GCHandle
die een verwijzing naar een exemplaar van de test.Test
klasse bevat.
De derde hoofdmap is een vastgemaakt GCHandle
. Deze is eigenlijk een statische variabele, maar helaas is er geen manier om het te vertellen. Statische gegevens voor referentietypen worden opgeslagen in een beheerde objectmatrix in interne runtimestructuren.
Een ander geval dat het lossen van een draad AssemblyLoadContext
kan voorkomen, is wanneer een draad een frame van een methode van een assembly heeft die in de AssemblyLoadContext
stapel is geladen. U kunt dit controleren door beheerde aanroepstacks van alle threads te dumpen:
~*e !clrstack
De opdracht betekent 'toepassen op alle threads van de !clrstack
opdracht'. Hier volgt de uitvoer van die opdracht voor het voorbeeld. Helaas heeft LLDB op Unix geen manier om een opdracht toe te passen op alle threads, dus u moet threads handmatig wisselen en de clrstack
opdracht herhalen. Negeer alle threads waarin het foutopsporingsprogramma 'Kan de beheerde stack niet doorlopen'.
OS Thread Id: 0x6ba8 (0)
Child SP IP Call Site
0000001fc697d5c8 00007ffb50d9de12 [HelperMethodFrame: 0000001fc697d5c8] System.Diagnostics.Debugger.BreakInternal()
0000001fc697d6d0 00007ffa864765fa System.Diagnostics.Debugger.Break()
0000001fc697d700 00007ffa864736bc example.Program.Main(System.String[]) [E:\unloadability\example\Program.cs @ 70]
0000001fc697d998 00007ffae5fdc1e3 [GCFrame: 0000001fc697d998]
0000001fc697df28 00007ffae5fdc1e3 [GCFrame: 0000001fc697df28]
OS Thread Id: 0x2ae4 (1)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x61a4 (2)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x7fdc (3)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5390 (4)
Unable to walk the managed stack. The current thread is likely not a
managed thread. You can run !threads to get a list of managed threads in
the process
Failed to start stack walk: 80070057
OS Thread Id: 0x5ec8 (5)
Child SP IP Call Site
0000001fc70ff6e0 00007ffb5437f6e4 [DebuggerU2MCatchHandlerFrame: 0000001fc70ff6e0]
OS Thread Id: 0x4624 (6)
Child SP IP Call Site
GetFrameContext failed: 1
0000000000000000 0000000000000000
OS Thread Id: 0x60bc (7)
Child SP IP Call Site
0000001fc727f158 00007ffb5437fce4 [HelperMethodFrame: 0000001fc727f158] System.Threading.Thread.SleepInternal(Int32)
0000001fc727f260 00007ffb37ea7c2b System.Threading.Thread.Sleep(Int32)
0000001fc727f290 00007ffa865005b3 test.Program.ThreadProc() [E:\unloadability\test\Program.cs @ 17]
0000001fc727f2c0 00007ffb37ea6a5b System.Threading.Thread.ThreadMain_ThreadStart()
0000001fc727f2f0 00007ffadbc4cbe3 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
0000001fc727f568 00007ffae5fdc1e3 [GCFrame: 0000001fc727f568]
0000001fc727f7f0 00007ffae5fdc1e3 [DebuggerU2MCatchHandlerFrame: 0000001fc727f7f0]
Zoals u kunt zien, heeft test.Program.ThreadProc()
de laatste thread . Dit is een functie van de assembly die in de AssemblyLoadContext
assembly is geladen, en dus blijft het AssemblyLoadContext
leven.
Voorbeeld van broncode
De volgende code die problemen met de laadbaarheid bevat, wordt gebruikt in het vorige voorbeeld van foutopsporing.
Hoofdtestprogramma
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
namespace example
{
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
class TestInfo
{
public TestInfo(MethodInfo? mi)
{
_entryPoint = mi;
}
MethodInfo? _entryPoint;
}
class Program
{
static TestInfo? entryPoint;
[MethodImpl(MethodImplOptions.NoInlining)]
static int ExecuteAndUnload(string assemblyPath, out WeakReference testAlcWeakRef, out MethodInfo? testEntryPoint)
{
var alc = new TestAssemblyLoadContext();
testAlcWeakRef = new WeakReference(alc);
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
if (a == null)
{
testEntryPoint = null;
Console.WriteLine("Loading the test assembly failed");
return -1;
}
var args = new object[1] {new string[] {"Hello"}};
// Issue preventing unloading #1 - we keep MethodInfo of a method
// for an assembly loaded into the TestAssemblyLoadContext in a static variable.
entryPoint = new TestInfo(a.EntryPoint);
testEntryPoint = a.EntryPoint;
var oResult = a.EntryPoint?.Invoke(null, args);
alc.Unload();
return (oResult is int result) ? result : -1;
}
static void Main(string[] args)
{
WeakReference testAlcWeakRef;
// Issue preventing unloading #2 - we keep MethodInfo of a method for an assembly loaded into the TestAssemblyLoadContext in a local variable
MethodInfo? testEntryPoint;
int result = ExecuteAndUnload(@"absolute/path/to/test.dll", out testAlcWeakRef, out testEntryPoint);
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
System.Diagnostics.Debugger.Break();
Console.WriteLine($"Test completed, result={result}, entryPoint: {testEntryPoint} unload success: {!testAlcWeakRef.IsAlive}");
}
}
}
Het programma is geladen in de TestAssemblyLoadContext
De volgende code vertegenwoordigt de test.dll doorgegeven aan de ExecuteAndUnload
methode in het hoofdtestprogramma.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace test
{
class Test
{
}
class Program
{
public static void ThreadProc()
{
// Issue preventing unloading #4 - a thread running method inside of the TestAssemblyLoadContext at the unload time
Thread.Sleep(Timeout.Infinite);
}
static GCHandle handle;
static int Main(string[] args)
{
// Issue preventing unloading #3 - normal GC handle
handle = GCHandle.Alloc(new Test());
Thread t = new Thread(new ThreadStart(ThreadProc));
t.IsBackground = true;
t.Start();
Console.WriteLine($"Hello from the test: args[0] = {args[0]}");
return 1;
}
}
}