Какова ценность AssemblyLoadContext.Unload() в .NET Core по сравнению с обычной сборкой мусора?

В .NET Core 3.0 введен коллекционный AssemblyLoadContext, который позволяет вызывать метод Unload() для выгрузки сборок, загруженных внутри контекста.

Согласно документации (https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability#troubleshoot-unloadability-issues), выгрузка является асинхронной, и любая ссылка на контекст или объекты из него предотвратит выгрузку контекста.

Мне было интересно, что, если я потеряю ссылку на AssemblyLoadContext, вызовет ли это утечку (поскольку у меня больше нет контекста для вызова Unload()). Тест показал, что это не приведет к утечке и неиспользуемая сборка будет выгружена даже без явного вызова Unload():

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using NUnit.Framework;

namespace Tests.Core
{
    [TestFixture]
    public class CollectibleAssemblyLoadContextTests
    {
        private const string AssemblyName = "Test___DynamicAssembly";

        [Test]
        [TestCase(/*unload*/ true,  /*GC sessions*/ 1)]
        [TestCase(/*unload*/ false, /*GC sessions*/ 2)]
        public void ShouldExecuteAndUnload(bool unload, int expectedGcSessions)
        {
            string actual = Execute(10, unload);
            Assert.AreEqual("executed 10", actual);

            int gcSessions = 0;
            while (!IsUnloaded())
            {
                GC.Collect();
                gcSessions++;
            }

            Assert.AreEqual(expectedGcSessions, gcSessions);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private bool IsUnloaded()
        {
            return !AppDomain.CurrentDomain.GetAssemblies()
                .Select(x => x.GetName().Name)
                .Contains(AssemblyName);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private string Execute(int number, bool unload)
        {
            var source = @"
        public static class Process
        {
            public static string Execute(int i)
            {
                return $""executed {i}"";
            }
        }";
            var compilation = CSharpCompilation.Create(AssemblyName, new[] {CSharpSyntaxTree.ParseText(source)},
                new []{MetadataReference.CreateFromFile(typeof(object).Assembly.Location)},
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using var ms = new MemoryStream();
            compilation.Emit(ms);
            ms.Seek(0, SeekOrigin.Begin);

            var assemblyLoadContext = new AssemblyLoadContext("CollectibleContext", isCollectible: true);

            Assembly assembly = assemblyLoadContext.LoadFromStream(ms);
            if (unload)
                assemblyLoadContext.Unload();

            Type type = assembly.GetType("Process");
            MethodInfo method = type.GetMethod("Execute");
            return (string)method.Invoke(null, new object[] {number});
        }
    }
}

Этот тест также показывает, что при использовании Unload() контекст выгружается после 1 сеанса GC, независимо от того, без Unload() для выгрузки требуется 2 сеанса. Но может быть просто совпадением и не всегда воспроизводимо.

Итак, учитывая, что

  1. Любые ссылки на собираемый контекст предотвратят его выгрузку (поэтому можно вызвать Unload() сразу после загрузки всех сборок, которые вам нужны, чтобы запланировать выгрузку, когда она не будет использоваться).
  2. Даже без вызова Unload() коллекционный контекст выгружается, как только он больше не используется.

Какова цель этого метода Unload() и в чем разница между использованием Unload() и простым использованием GC?


person Ivan Shimko    schedule 17.02.2020    source источник
comment
Это похоже на тот же вопрос, что и вопрос о том, какова цель метода Dispose() и в чем разница между использованием Dispose() и простым использованием GC?   -  person Lasse V. Karlsen    schedule 17.02.2020
comment
@ LasseV.Karlsen Не совсем. Dispose обычно является синхронным и не запланирован здесь как Unload. Я могу вызвать Unload сразу после загрузки всех вещей в него, и в любом случае мне придется ждать, пока сборщик мусора соберет его (и этого не произойдет, пока контекст используется). Я не могу следовать тому же шаблону для Dispose.   -  person Ivan Shimko    schedule 17.02.2020


Ответы (1)


У меня было такое же впечатление, и тесты, которые я сделал, показали то же самое. Похоже, явно вызывать Unload() не имеет смысла. Затем я нашел https://github.com/dotnet/samples/blob/master/core/tutorials/Unloading и понял, что если вы прокомментируете там Unload(), библиотека не будет выгружена. Это строка https://github.com/dotnet/samples/blob/master/core/tutorials/Unloading/Host/Program.cs#L74. В конце концов, это так, как говорит Лассе В. Карлсен. Если вы вызываете Unload() явно, вы можете ожидать, что ваша библиотека будет выгружена быстрее.

person Serg046    schedule 25.08.2020