.NET'te derleme kaldırılabilirliğini kullanma ve hatalarını ayıklama
.NET (Core), bir dizi derlemeyi yükleme ve daha sonra kaldırma özelliğini kullanıma sunar. .NET Framework'te bu amaçla özel uygulama etki alanları kullanılmıştır, ancak .NET (Core) yalnızca tek bir varsayılan uygulama etki alanını destekler.
Kaldırılabilirlik, aracılığıyla AssemblyLoadContextdesteklenir. Bir derleme kümesini bir collectible AssemblyLoadContext
içine yükleyebilir, içinde yöntemler yürütebilir veya yalnızca yansıma kullanarak inceleyebilir ve son olarak öğesini kaldırabilirsiniz AssemblyLoadContext
. Bu, içine AssemblyLoadContext
yüklenen derlemeleri kaldırır.
AppDomains'i kullanarak AssemblyLoadContext
kaldırma ile kullanma arasında önemli bir fark vardır. AppDomains ile kaldırma zorlanır. Yükleme kaldırıldığında, hedef AppDomain'de çalışan tüm iş parçacıkları durduruldu, hedef AppDomain'de oluşturulan yönetilen COM nesneleri yok edilir ve bu şekilde devam eder. ile AssemblyLoadContext
, boşaltma "kooperatiftir". yöntemini çağırmak AssemblyLoadContext.Unload yalnızca kaldırma işlemini başlatır. Kaldırma işlemi şu süre sonunda tamamlar:
- Hiçbir iş parçacığının çağrı yığınlarında içine
AssemblyLoadContext
yüklenen derlemelerden yöntemleri yoktur. - içine yüklenen
AssemblyLoadContext
derlemelerdeki türlerden hiçbiri, bu türlerin örnekleri ve derlemelerin kendilerine şu şekilde başvuruda bulunur:- Zayıf başvurular
AssemblyLoadContext
(WeakReference veya WeakReference<T>) dışında başvurular. - hem içinden hem de dışından güçlü çöp toplayıcı (GC) tanıtıcıları (GCHandleType.Normal veya GCHandleType.Pinned).
AssemblyLoadContext
- Zayıf başvurular
Collectible AssemblyLoadContext kullanma
Bu bölüm, bir .NET (Core) uygulamasını bir koleksiyona AssemblyLoadContext
yüklemenin, giriş noktasını yürütmenin ve ardından kaldırmanın basit bir yolunu gösteren ayrıntılı bir adım adım öğretici içerir. Tam bir örneği adresinde https://github.com/dotnet/samples/tree/main/core/tutorials/Unloadingbulabilirsiniz.
Collectible AssemblyLoadContext oluşturma
sınıfından AssemblyLoadContext sınıfınızı türetin ve yöntemini geçersiz kılın AssemblyLoadContext.Load . Bu yöntem, içine yüklenen AssemblyLoadContext
derlemelerin bağımlılıkları olan tüm derlemelere başvuruları çözümler.
Aşağıdaki kod, en basit özel AssemblyLoadContext
örneğidir:
class TestAssemblyLoadContext : AssemblyLoadContext
{
public TestAssemblyLoadContext() : base(isCollectible: true)
{
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
Load
Gördüğünüz gibi yöntemi döndürürnull
. Bu, tüm bağımlılık derlemelerinin varsayılan bağlama yüklendiği ve yeni bağlamın yalnızca açıkça yüklenen derlemeleri içerdiği anlamına gelir.
Bağımlılıkların bir kısmını veya tümünü de içine AssemblyLoadContext
yüklemek istiyorsanız yöntemini kullanabilirsiniz AssemblyDependencyResolver
Load
. , AssemblyDependencyResolver
derleme adlarını mutlak derleme dosya yollarına çözümler. Çözümleyici, bağlama yüklenen ana derlemenin dizinindeki .deps.json dosyasını ve derleme dosyalarını kullanır.
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;
}
}
}
Özel bir collectible AssemblyLoadContext kullanma
Bu bölümde, daha basit sürümünün TestAssemblyLoadContext
kullanıldığı varsayılır.
Özel AssemblyLoadContext
örneğini oluşturabilir ve içine aşağıdaki gibi bir derleme yükleyebilirsiniz:
var alc = new TestAssemblyLoadContext();
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
Yüklenen derleme tarafından başvurulan derlemelerin her biri için yöntemi TestAssemblyLoadContext.Load
çağrılır, böylece TestAssemblyLoadContext
derlemenin nereden alınacağına karar verebilir. Bu durumda, çalışma zamanının derlemeleri varsayılan olarak yüklemek için kullandığı konumlardan varsayılan bağlama yüklenmesi gerektiğini belirtmek için döndürür null
.
Artık bir derleme yüklendiğinden, bundan bir yöntem yürütebilirsiniz. Main
yöntemini çalıştırın:
var args = new object[1] {new string[] {"Hello"}};
_ = a.EntryPoint?.Invoke(null, args);
yöntemi döndürdüğündeMain
, özel AssemblyLoadContext
üzerindeki yöntemini çağırarak Unload
veya öğesine sahip AssemblyLoadContext
olduğunuz başvuruyu kaldırarak kaldırma işlemini başlatabilirsiniz:
alc.Unload();
Bu, test derlemesini kaldırmak için yeterlidir. Ardından, yığın yuvası başvuruları (gerçek veya JIT ile tanıtılan yerel öğeler) tarafından , Assembly
ve MethodInfo
() değerlerinin Assembly.EntryPoint
canlı tutulamamasını sağlamak TestAssemblyLoadContext
için bunların tümünü ayrı bir satırlanamayan yönteme koyacaksınız. Bu, canlı kalmasını TestAssemblyLoadContext
sağlayabilir ve boşaltmayı önleyebilir.
Ayrıca, daha sonra kaldırma işlemini algılamak için AssemblyLoadContext
kullanabilmeniz için zayıf bir başvuru döndürebilirsiniz.
[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();
}
Artık derlemeyi yüklemek, yürütmek ve kaldırmak için bu işlevi çalıştırabilirsiniz.
WeakReference testAlcWeakRef;
ExecuteAndUnload("absolute/path/to/your/assembly", out testAlcWeakRef);
Ancak, kaldırma işlemi hemen tamamlanmaz. Daha önce belirtildiği gibi, test derlemesinden tüm nesneleri toplamak için çöp toplayıcıya dayanır. Çoğu durumda, kaldırma işleminin tamamlanmasını beklemek gerekmez. Ancak, kaldırma işleminin tamamlandığını bilmenin yararlı olduğu durumlar vardır. Örneğin, özel AssemblyLoadContext
diske yüklenen derleme dosyasını diskten silmek isteyebilirsiniz. Böyle bir durumda aşağıdaki kod parçacığı kullanılabilir. Çöp toplamayı tetikler ve özel AssemblyLoadContext
zayıf başvuru hedef nesnenin toplandığını belirten olarak ayarlanana null
kadar döngüde bekleyen sonlandırıcıları bekler. Çoğu durumda, döngüden yalnızca bir geçiş gereklidir. Ancak, içinde AssemblyLoadContext
çalıştırılan kod tarafından oluşturulan nesnelerin sonlandırıcılara sahip olduğu daha karmaşık durumlar için daha fazla geçiş gerekebilir.
for (int i = 0; testAlcWeakRef.IsAlive && (i < 10); i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Sınırlamalar
Bir koleksiyona yüklenen derlemeler, AssemblyLoadContext
toplanabilir derlemelerdeki genel kısıtlamalara uymalıdır. Ayrıca aşağıdaki sınırlamalar da geçerlidir:
- C++/CLI ile yazılan derlemeler desteklenmez.
- ReadyToRun tarafından oluşturulan kod yoksayılır.
Kaldırılan olay
Bazı durumlarda, bir özel AssemblyLoadContext
dosyaya yüklenen kodun, kaldırma işlemi başlatıldığında biraz temizleme gerçekleştirmesi gerekebilir. Örneğin, iş parçacıklarını durdurması veya güçlü GC tanıtıcılarını temizlemesi gerekebilir. Olay bu Unloading
gibi durumlarda kullanılabilir. Bu olay için gerekli temizlemeyi gerçekleştiren bir işleyici bağlayabilirsiniz.
Kaldırılabilirlik sorunlarını giderme
Boşaltmanın işbirlikçi yapısı nedeniyle, eşyaları bir toplama kutusunda AssemblyLoadContext
canlı tutan ve boşaltmayı önleyen referansları unutmak kolaydır. Aşağıda, başvuruları tutabilen varlıkların (bazıları gizli olmayan) bir özeti yer alır:
- Bir yığın yuvasında veya işlemci yazmacında depolanan collectible
AssemblyLoadContext
dışından tutulan düzenli başvurular (yöntem yerel öğeleri, kullanıcı kodu tarafından açıkça oluşturulmuş veya tam zamanında (JIT) derleyici tarafından örtük olarak), statik bir değişken veya güçlü (sabitleme) GC tutamacı ve geçişli olarak işaret eden:- Collectible
AssemblyLoadContext
içine yüklenen bir derleme. - Böyle bir derlemeden tür.
- Böyle bir derlemeden tür örneği.
- Collectible
- Collectible
AssemblyLoadContext
içine yüklenen bir derlemeden kod çalıştıran iş parçacıkları. - Collectible
AssemblyLoadContext
içinde oluşturulan özel, toplanamazAssemblyLoadContext
türlerin örnekleri. - Özel içindeki yöntemlere ayarlanmış geri çağırmaları
AssemblyLoadContext
olan bekleyen RegisteredWaitHandle örnekler.
İpucu
Yığın yuvalarında veya işlemci kayıtlarında depolanan ve 'nin AssemblyLoadContext
kaldırılmasını önleyebilecek nesne başvuruları aşağıdaki durumlarda oluşabilir:
- İşlev çağrısı sonuçları, kullanıcı tarafından oluşturulan yerel değişken olmasa bile doğrudan başka bir işleve geçirildiğinde.
- JIT derleyicisi bir yöntemin belirli bir noktasında kullanılabilen bir nesneye başvuruyu tuttuğunda.
Kaldırma sorunlarının hatalarını ayıklama
Kaldırmayla ilgili hata ayıklama sorunları yorucu olabilir. Bir canlıyı neyin tutabileceğini AssemblyLoadContext
bilmediğiniz, ancak boşaltmanın başarısız olduğu durumlara girebilirsiniz. Bu konuda yardımcı olabilecek en iyi araç, SOS eklentisine sahip WinDbg (veya Unix'te LLDB). Belirli bir LoaderAllocator
şeye ait AssemblyLoadContext
olanı canlı tutan şeyi bulmalısın. SOS eklentisi GC yığın nesnelerine, hiyerarşilerine ve köklerine bakmanızı sağlar.
SOS eklentisini hata ayıklayıcıya yüklemek için hata ayıklayıcı komut satırına aşağıdaki komutlardan birini girin.
WinDbg'de (henüz yüklenmemişse):
.loadby sos coreclr
LLDB'de:
plugin load /path/to/libsosplugin.so
Şimdi yükleme kaldırma sorunları olan örnek bir programın hatalarını ayıklayacaksınız. Kaynak kodu Örnek kaynak kodu bölümünde bulunur. WinDbg altında çalıştırdığınızda, yükleme kaldırma başarısını denetlemeye çalıştıktan hemen sonra program hata ayıklayıcıya girer. Ardından suçluları aramaya başlayabilirsiniz.
İpucu
Unix'te LLDB kullanarak hata ayıklarsanız, aşağıdaki örneklerde !
yer alan SOS komutlarının önünde yok.
!dumpheap -type LoaderAllocator
Bu komut, GC yığınında bulunan LoaderAllocator
bir tür adına sahip tüm nesneleri döküm eder. Bir örnek aşağıda verilmiştir:
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
"İstatistikler:" bölümünde, ilgilendiğiniz nesne olan öğesine ait System.Reflection.LoaderAllocator
() öğesini denetleyin.MT
MethodTable
Ardından, başlangıçtaki listede, bununla eşleşen girdiyi MT
bulun ve nesnenin adresini alın. Bu durumda, "000002b78000ce40" olur.
Artık nesnenin adresini bildiğinize LoaderAllocator
göre, GC köklerini bulmak için başka bir komut kullanabilirsiniz:
!gcroot 0x000002b78000ce40
Bu komut, örneğe yol açan nesne başvuruları zincirini döküm eder LoaderAllocator
. Liste, canlı tutan LoaderAllocator
varlık olan kök ile başlar ve bu nedenle sorunun temelidir. Kök bir yığın yuvası, işlemci yazmacı, GC tutamacı veya statik değişken olabilir.
Aşağıda komutun çıktısının bir örneği verilmişti 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.
Bir sonraki adım, kökü düzeltebilmeniz için kökün nerede bulunduğunu bulmaktır. En kolay durum, kökün bir yığın yuvası veya işlemci yazmaç olmasıdır. Bu durumda, gcroot
çerçevesi kökü içeren işlevin adını ve bu işlevi yürüten iş parçacığını gösterir. Kök statik değişken veya GC tanıtıcısı olduğunda zor bir durum söz konusudur.
Önceki örnekte ilk kök, işlevin çerçevesinde adreste rbp-20
depolanan türün System.Reflection.RuntimeMethodInfo
yerel bir köküdür (rbp
işlemci yazmacıdır ve -20, bu yazmacın rbp
onaltılık uzaklığıdır).example.Program.Main(System.String[])
İkinci kök, sınıfın bir örneğine başvuru tutan normal (güçlü) GCHandle
bir kökdür test.Test
.
Üçüncü kök sabitlenmiş GCHandle
bir . Bu aslında statik bir değişkendir, ancak ne yazık ki bunu anlamanın bir yolu yok. Başvuru türleri için statikler, iç çalışma zamanı yapılarındaki yönetilen nesne dizisinde depolanır.
Bir iş parçacığının AssemblyLoadContext
kaldırılmasını önleyebilen bir diğer durum, bir iş parçacığının yığınına yüklenmiş bir derlemeden bir yöntem çerçevesine AssemblyLoadContext
sahip olmasıdır. Tüm iş parçacıklarının yönetilen çağrı yığınlarının dökümünü alarak bunu de kontrol edebilirsiniz:
~*e !clrstack
komutu "komutun tüm iş parçacıklarına !clrstack
uygula" anlamına gelir. Aşağıda, örnek için bu komutun çıkışı verilmiştir. Ne yazık ki, Unix'te LLDB'nin tüm iş parçacıklarına komut uygulamak için herhangi bir yolu yoktur, bu nedenle iş parçacıklarını el ile değiştirip komutu yinelemeniz clrstack
gerekir. Hata ayıklayıcının "Yönetilen yığında adım adım izlenemiyor" sözlerini söylediği tüm iş parçacıklarını yoksayın.
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]
Gördüğünüz gibi, son iş parçacığında vardır test.Program.ThreadProc()
. Bu, içine AssemblyLoadContext
yüklenen derlemeden bir işlevdir ve bu nedenle canlı kalmasını sağlar AssemblyLoadContext
.
Örnek kaynak kodu
Kaldırılabilirlik sorunları içeren aşağıdaki kod, önceki hata ayıklama örneğinde kullanılır.
Ana test 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}");
}
}
}
TestAssemblyLoadContext içine yüklenen program
Aşağıdaki kod, ana test programında yöntemine ExecuteAndUnload
geçirilen test.dll temsil eder.
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;
}
}
}