Megosztás a következőn keresztül:


Szerelvény eltávolíthatóságának használata és hibakeresése a .NET-ben

A .NET (Core) lehetővé teszi a szerelvények betöltését és későbbi eltávolítását. A .NET-keretrendszer egyéni alkalmazástartományokat használtak erre a célra, de a .NET (Core) csak egyetlen alapértelmezett alkalmazástartományt támogat.

A kiüríthetőséget a . AssemblyLoadContexttámogatja. A szerelvényeket be lehet tölteni egy gyűjthetőbe AssemblyLoadContext, metódusokat hajthat végre bennük, vagy megvizsgálhatja őket visszatükrözéssel, és végül eltávolíthatja a AssemblyLoadContext. Ez eltávolítja a berakott szerelvényeket a AssemblyLoadContext.

Van egy figyelemre méltó különbség az AppDomains használatával AssemblyLoadContext és használatával történő kirakodás között. Az AppDomains használatával a kiürítés kényszerítve van. A kiürítéskor a cél AppDomainben futó összes szál megszakad, a cél AppDomainben létrehozott felügyelt COM-objektumok el lesznek pusztítva, és így tovább. Ezzel AssemblyLoadContexta "kooperatív" a kipakolás. A metódus meghívása AssemblyLoadContext.Unload csak a kiürítést kezdeményezi. A kiürítés a következő után fejeződik be:

  • Egyetlen szál sem rendelkezik metódusokkal a hívásverembe AssemblyLoadContext betöltött szerelvényekből.
  • Az ilyen típusú szerelvényekbe betöltött AssemblyLoadContextszerelvények egyik típusára sem, és maguk a szerelvényekre a következők hivatkoznak:

Collectible AssemblyLoadContext használata

Ez a szakasz részletes részletes oktatóanyagot tartalmaz, amely bemutatja, hogyan tölthet be egy .NET-alkalmazást (Core-alkalmazást) egy gyűjthetőbe, végrehajthatja AssemblyLoadContexta belépési pontját, majd eltávolíthatja azt. A teljes minta a következő helyen https://github.com/dotnet/samples/tree/main/core/tutorials/Unloadingtalálható: .

Gyűjthető AssemblyLoadContext létrehozása

Származtathatja az osztályt a AssemblyLoadContext metódusból, és felülbírálhatja annak metódusát AssemblyLoadContext.Load . Ez a módszer feloldja az összes olyan szerelvényre mutató hivatkozást, amely az ebbe AssemblyLoadContextbetöltött szerelvények függőségei.

Az alábbi kód a legegyszerűbb egyéni AssemblyLoadContextpélda:

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

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

Mint látható, a Load metódus ad vissza null. Ez azt jelenti, hogy az összes függőségi szerelvény betöltődik az alapértelmezett környezetbe, és az új környezet csak a kifejezetten betöltött szerelvényeket tartalmazza.

Ha a függőségek egy részét vagy egészét is be AssemblyLoadContext szeretné tölteni, a metódusban használhatja.AssemblyDependencyResolverLoad A AssemblyDependencyResolver szerelvénynevek feloldása abszolút szerelvényfájl-elérési utakra. A feloldó a környezetbe betöltött főszerelvény könyvtárában található .deps.json fájl- és szerelvényfájlokat használja.

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

Egyéni gyűjthető AssemblyLoadContext használata

Ez a szakasz feltételezi, hogy a rendszer a TestAssemblyLoadContext használt egyszerűbb verziót használja.

Létrehozhatja az egyéni AssemblyLoadContext példányt, és az alábbiak szerint tölthet be egy szerelvényt:

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

A rendszer meghívja a metódust a betöltött szerelvény által hivatkozott szerelvények mindegyikére, TestAssemblyLoadContext.Load hogy eldönthesse TestAssemblyLoadContext , honnan szerezze be a szerelvényt. Ebben az esetben a rendszer azt null jelzi, hogy az alapértelmezett környezetbe kell betölteni olyan helyekről, amelyeket a futtatókörnyezet alapértelmezés szerint a szerelvények betöltésére használ.

Most, hogy egy szerelvény be lett töltve, végrehajthat belőle egy metódust. Futtassa a metódust Main :

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

A metódus visszatérése után kezdeményezheti a Main kiürítést, ha meghívja a Unload metódust az egyénire AssemblyLoadContext , vagy eltávolítja a következőre AssemblyLoadContextmutató hivatkozást:

alc.Unload();

Ez elegendő a tesztszerelvény eltávolításához. Ezután mindezt egy külön, nem elválasztható metódusba helyezi, amely biztosítja, hogy a TestAssemblyLoadContext, Assemblyés MethodInfo (a Assembly.EntryPoint) nem tartható életben veremhelyhivatkozásokkal (valós vagy JIT által bevezetett helyiek). Ez életben tarthatja az TestAssemblyLoadContext embert, és megakadályozhatja a kirakodást.

Emellett adjon vissza egy gyenge hivatkozást a AssemblyLoadContext fájlra, hogy később használhassa a kipakolás befejezésének észleléséhez.

[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();
}

Most futtathatja ezt a függvényt a szerelvény betöltéséhez, végrehajtásához és eltávolításához.

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

A kirakodás azonban nem fejeződik be azonnal. Mint korábban említettük, a szemétgyűjtőre támaszkodik, hogy összegyűjtse az összes objektumot a tesztszerelvényből. Sok esetben nem szükséges megvárni a kiürítés befejezését. Vannak azonban olyan esetek, amikor hasznos tudni, hogy a kirakodás befejeződött. Előfordulhat például, hogy törölni szeretné a lemezről az egyénibe AssemblyLoadContext betöltött szerelvényfájlt. Ilyen esetben a következő kódrészlet használható. Elindítja a szemétgyűjtést, és várakozik a függőben lévő véglegesítőkre egy hurokban, amíg az egyénire AssemblyLoadContext mutató gyenge hivatkozás be nem van állítva null, jelezve, hogy a célobjektum összegyűjtve lett. A legtöbb esetben csak egy áthaladás szükséges a hurokon. Összetettebb esetekben azonban, amikor a kód AssemblyLoadContext által létrehozott objektumok véglegesítőkkel rendelkeznek, több passzra lehet szükség.

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

Korlátozások

A gyűjthető szerelvényekre vonatkozó általános korlátozásoknak meg kell felelniük a gyűjthető AssemblyLoadContext szerelvényekre vonatkozó általános korlátozásoknak. A következő korlátozások is érvényesek:

  • A C++/CLI nyelven írt szerelvények nem támogatottak.
  • A ReadyToRun által létrehozott kód figyelmen kívül lesz hagyva.

A kiürítési esemény

Bizonyos esetekben szükség lehet arra, hogy a kód be legyen töltve egy egyéni AssemblyLoadContext fájlba a törlés indításakor. Előfordulhat például, hogy le kell állítania a szálakat, vagy törölnie kell az erős GC-fogantyúkat. Ilyen Unloading esetekben az esemény használható. Csatlakoztathat egy kezelőt, amely elvégzi a szükséges tisztítást az eseményhez.

Eltávolíthatósági problémák elhárítása

A kirakodás kooperatív jellege miatt könnyű elfelejteni azokat a hivatkozásokat, amelyek életben tartják a cuccot egy gyűjtőben AssemblyLoadContext , és megakadályozzák a kirakodást. Íme az entitások összegzése (amelyek némelyike nem látható), amelyek tartalmazhatják a hivatkozásokat:

  • A gyűjtőn AssemblyLoadContext kívülről tárolt rendszeres hivatkozások, amelyek egy veremhelyen vagy egy processzorregisztrálóban vannak tárolva (a metódus helyi beállításai, amelyeket explicit módon a felhasználói kód hoz létre, vagy implicit módon a just-in-time (JIT) fordító), egy statikus változó vagy egy erős (rögzítési) GC-leíró, és tranzitív módon a következőkre mutat:
    • A gyűjtőbe AssemblyLoadContextbetöltött szerelvény.
    • Egy ilyen szerelvény típusa.
    • Egy ilyen szerelvényből származó típuspéldány.
  • A gyűjtőbe AssemblyLoadContextbetöltött szerelvényből kódot futtató szálak.
  • A gyűjteményben AssemblyLoadContextlétrehozott egyéni, nem értelmezhető AssemblyLoadContext típusok példányai.
  • Függőben lévő RegisteredWaitHandle példányok, amelyben a visszahívások az egyéni AssemblyLoadContextmetódusokra van beállítva.

Tipp.

A veremhelyeken vagy a processzorregisztrálókban tárolt objektumhivatkozások, amelyek megakadályozhatják a törlést AssemblyLoadContext , a következő helyzetekben fordulhatnak elő:

  • Ha a függvényhívás eredményei közvetlenül egy másik függvénynek lesznek átadva, annak ellenére, hogy nincs felhasználó által létrehozott helyi változó.
  • Amikor a JIT-fordító egy olyan objektumra mutat, amely egy metódus egy bizonyos pontján elérhető volt.

Törlési problémák hibakeresése

A kiürítéssel kapcsolatos hibakeresési problémák fárasztóak lehetnek. Olyan helyzetekbe kerülhet, amikor nem tudja, mi tartható AssemblyLoadContext életben, de a kirakodás meghiúsul. A legjobb eszköz, hogy segítsen, hogy a WinDbg (vagy LLDB a Unix) az SOS beépülő modul. Meg kell találnia, hogy mi tart egy LoaderAllocator adott élőhöz AssemblyLoadContext tartozót. Az SOS beépülő modul segítségével áttekintheti a GC halomobjektumokat, azok hierarchiáit és gyökereit.

Az SOS beépülő modul hibakeresőbe való betöltéséhez írja be az alábbi parancsok egyikét a hibakereső parancssorába.

WinDbg-ben (ha még nincs betöltve):

.loadby sos coreclr

AZ LLDB-ben:

plugin load /path/to/libsosplugin.so

Most egy olyan példaprogramot fog hibakeresésre végezni, amely problémákat tapasztal a kiürítéssel kapcsolatban. A forráskód a Példa forráskód szakaszban érhető el. Amikor a WinDbg alatt futtatja, a program közvetlenül azután tör be a hibakeresőbe, hogy megkísérelte ellenőrizni a törlés sikerességét. Ezután elkezdheti keresni a bűnösöket.

Tipp.

Ha az LLDB használatával hibakeresést hajt végre a Unixon, az alábbi példák SOS-parancsai nem eléjük vannak ! beállítva.

!dumpheap -type LoaderAllocator

Ez a parancs a GC-halomtárban található típusnévvel rendelkező LoaderAllocator összes objektumot kiírja. Példa:

         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

A "Statisztika:" részben ellenőrizze a MTMethodTable() objektumot, amely a System.Reflection.LoaderAllocatorfontos objektumhoz tartozik. Ezután az elején található listában keresse meg azt a bejegyzést, amely MT megfelel az adottnak, és kérje le magának az objektumnak a címét. Ebben az esetben ez "000002b78000ce40".

Most, hogy megismerte az LoaderAllocator objektum címét, egy másik paranccsal megkeresheti annak GC-gyökerét:

!gcroot 0x000002b78000ce40

Ez a parancs a példányhoz vezető objektumhivatkozások láncolatát LoaderAllocator dobja ki. A lista a gyökérrel kezdődik, amely az az entitás, amely életben tartja a LoaderAllocator problémát, és így a probléma magja. A gyökér lehet veremhely, processzorregisztrálás, GC-leíró vagy statikus változó.

Íme egy példa a parancs kimenetére gcroot :

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.

A következő lépés annak kiderítése, hogy hol található a gyökér, hogy kijavíthassa. A legegyszerűbb eset az, ha a gyökér egy veremhely vagy egy processzorregisztrálás. Ebben az esetben a gcroot függvény neve jelenik meg, amelynek kerete tartalmazza a függvényt végrehajtó gyökér- és szálat. A nehéz eset az, ha a gyökér statikus változó vagy GC-leíró.

Az előző példában az első gyökér a címben található függvény example.Program.Main(System.String[]) keretében tárolt helyi típusú System.Reflection.RuntimeMethodInfo (rbp a processzorregisztrálásrbp, a -20 pedig a regiszter hexadecimális eltolása).rbp-20

A második gyökér egy normál (erős), GCHandle amely az osztály egy példányára test.Test mutató hivatkozást tartalmaz.

A harmadik gyökér egy rögzített GCHandle. Ez valójában egy statikus változó, de sajnos nem lehet megmondani. A referenciatípusok statikus elemeit egy felügyelt objektumtömb tárolja belső futtatókörnyezeti struktúrákban.

Egy másik eset, amely megakadályozhatja a kipakolást, AssemblyLoadContext ha egy szál egy metóduskerettel rendelkezik a verembe AssemblyLoadContext betöltött szerelvényből. Ezt az összes szál felügyelt hívásveremeinek memóriaképével ellenőrizheti:

~*e !clrstack

A parancs azt jelenti, hogy "a parancs minden szálára !clrstack érvényes". A példában a következő parancs kimenete látható. Sajnos a Unix LLDB-jének nincs módja arra, hogy parancsot alkalmazzon az összes szálra, ezért manuálisan kell váltania a szálakat, és meg kell ismételnie a clrstack parancsot. Hagyja figyelmen kívül azokat a szálakat, ahol a hibakereső a következőhöz hasonló szöveget írja: "Nem lehet a felügyelt vermet járni".

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]

Mint látható, az utolsó szál van test.Program.ThreadProc(). Ez egy függvény a szerelvény berakottAssemblyLoadContext, és így tartja életben.AssemblyLoadContext

Példa forráskódra

Az előző hibakeresési példában a következő, eltávolíthatósági problémákat tartalmazó kódot használjuk.

Fő tesztelési 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;

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

A TestAssemblyLoadContextbe betöltött program

Az alábbi kód a fő tesztelési programban a metódusnak ExecuteAndUnload átadott test.dll jelöli.

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