New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unload assembly from runtime #19773

Closed
bartstinson opened this Issue May 15, 2017 · 44 comments

Comments

Projects
None yet
@bartstinson
Copy link

bartstinson commented May 15, 2017

Hello,

Now that AppDomains have been removed from .NET Core, how does one unload an assembly from memory once it has been loaded? AssemblyLoadContext has an Unloading event but no way to programmatically trigger the unload of an assembly

@davidfowl

This comment has been minimized.

Copy link
Contributor

davidfowl commented May 15, 2017

You can't right now AFAIK.

@danmosemsft

This comment has been minimized.

Copy link
Member

danmosemsft commented May 15, 2017

@gkhanna79 can you confirm that this is not expected to be supported in Core?

@bartstinson what are you trying to achieve?

@bartstinson

This comment has been minimized.

Copy link

bartstinson commented May 15, 2017

@danmosemsft I need the ability to unload a dll assembly because I may have an updated version of that dll to load and I don't want to tear down the entire process to do so. I was able to do this with AppDomains before and with AppDomains gone in .NET Core there doesn't seem to be a replacement.

@davidfowl

This comment has been minimized.

Copy link
Contributor

davidfowl commented May 15, 2017

@bartstinson Is that dll generated at runtime or does it have a fixed name?

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented May 15, 2017

I need the ability to unload a dll assembly because I may have an updated version of that dll to load and I don't want to tear down the entire process to do so

You can load updated copy of the .dll in a new assembly load context (https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/assemblyloadcontext.md), and keep the old copy loaded. Of course, this only works if you do not keep doing it again and again.

can you confirm that this is not expected to be supported in Core?

Unloading is not supported in .NET Core today. dotnet/coreclr#552 is the proposed plan to add it.

@jkotas jkotas added the question label May 15, 2017

@bartstinson

This comment has been minimized.

Copy link

bartstinson commented May 15, 2017

@davidfowl dll has fixed name.

@jkotas Thanks for the link. Are there any sample code or guides for how to use this LoadContext?

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented May 15, 2017

If you just need to load a single assembly, a good sample is implementation of Assembly.LoadFile .NET Standard 2.0 API using AssemblyLoadContext:

https://github.com/dotnet/coreclr/blob/b38113c80d04c39890207d149bf0359a86711d62/src/mscorlib/src/System/Runtime/Loader/AssemblyLoadContext.cs#L471
https://github.com/dotnet/coreclr/blob/3ababc21ab334a2e37c6ba4115c946ea26a6f2fb/src/mscorlib/src/System/Reflection/Assembly.CoreCLR.cs#L232

The basic structure is:

internal class MyAssemblyLoadContext : AssemblyLoadContext
{
   internal MyAssemblyLoadContext()
   {
   }

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

...
   AssemblyLoadContext alc = new MyAssemblyLoadContext();
   result = alc.LoadFromAssemblyPath(path);
...

The tests https://github.com/dotnet/corefx/blob/master/src/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs have more advanced examples.

@bartstinson

This comment has been minimized.

Copy link

bartstinson commented May 15, 2017

@jkotas Thanks. That helps a lot. Will monitor the threads for unload functionality

@karelz karelz modified the milestone: 2.0.0 May 18, 2017

@craigajohnson

This comment has been minimized.

Copy link
Contributor

craigajohnson commented Oct 31, 2017

AssemblyLoadContext is convenient and fast. However, the inability to unload a context causes enormous problems for plug-in scenarios. Orphaned contexts reside in memory until the process is dumped.

It's disappointing this didn't make it for 2.0. Is there an issue somewhere that is being tracked (or is THIS the issue)?

@terrajobst any ideas on progress here?

@poizan42

This comment has been minimized.

Copy link
Collaborator

poizan42 commented Nov 14, 2017

@agatlin

This comment has been minimized.

Copy link

agatlin commented Dec 27, 2017

It is very important to be able to dynamically load and unload assemblies. I have a situation where I dynamically load one or more of MANY (as in >10^2) assemblies for specialized processing of selected data. Once the data has been processed, those assemblies are no longer needed (until some relatively distant future time) and can and should be unloaded. Loading all of those assemblies in memory at once would be impractical. Merging them is also impractical (and architecturally unwise.) I would very much like to see Microsoft add this functionality back. It was there before for a reason. It is still needed.

This is a perfect example of how when an organization attempts to rewrite an existing piece of software they often leave out functionality because they were unable to understand the initial reason it was added to begin with.

@jnm2

This comment has been minimized.

Copy link
Collaborator

jnm2 commented Dec 29, 2017

I mean... it might be better than rewriting the functionality without understanding the initial reason it was added to begin with. 😜

@agatlin

This comment has been minimized.

Copy link

agatlin commented Mar 2, 2018

Is there any update on this functionality? Is there yet any way to unload an assembly in .NET Core? Is this functionality even planned?

Why is it actually becoming easier to actually work in C++ than C#? Seriously. Does anyone else notice the irony?

@per-samuelsson

This comment has been minimized.

Copy link

per-samuelsson commented Mar 7, 2018

Is there any update on this functionality? Is there yet any way to unload an assembly in .NET Core? Is this functionality even planned?

Don't look very promising, considering this comment: dotnet/coreclr#8677 (comment)

@seriouz

This comment has been minimized.

Copy link

seriouz commented Mar 30, 2018

This is a must-have feature when dotnet core should be a modern, useful and competitive language!

@Sigvaard

This comment has been minimized.

Copy link

Sigvaard commented Jun 29, 2018

Why is this closed anyway?

@MgSam

This comment has been minimized.

Copy link

MgSam commented Oct 4, 2018

In the other thread MS has made it clear that this is planned for .NET Core 3.0.

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Oct 4, 2018

In the other thread MS has made it clear that this is planned for .NET Core 3.0.

This is in the .NET Core 3.0 nightly builds now. Please give it a try and give us feedback.

https://github.com/dotnet/corefx/blob/master/Documentation/project-docs/dogfooding.md

@uffebjorklund

This comment has been minimized.

Copy link

uffebjorklund commented Oct 30, 2018

@jkotas Runnning 3.0.100-alpha1-009708 is assembly unloading expected to work in this version? Any pointers on how to test this in a good way?

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Oct 30, 2018

@janvorli Could you please share an example of what works in the current builds?

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Oct 31, 2018

Here is a simple example that loads, executes and unloads a simple "hello world" program in a loop:

using System;
using System.Reflection;
using System.Runtime.Loader;

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

    protected override Assembly Load(AssemblyName assemblyName) => null;
}

class Program
{
    private static int ExecuteAssembly(Assembly assembly, string[] args)
    {
        MethodInfo entry = assembly.EntryPoint;

        object result = entry.GetParameters().Length > 0 ?
                    entry.Invoke(null, new object[] { args }) :
                    entry.Invoke(null, null);

        return (result != null) ? (int)result : 0;
    }

    static void Main(string[] args)
    {
        for (;;) {
            var context = new SimpleUnloadableAssemblyLoadContext();
            Assembly assembly = context.LoadFromAssemblyPath(@"D:\repro\hello\bin\Debug\netcoreapp3.0\hello.dll");
            ExecuteAssembly(assembly, Array.Empty<string>());
            context.Unload();
        }
    }
}
@uffebjorklund

This comment has been minimized.

Copy link

uffebjorklund commented Oct 31, 2018

Works very well! No increase in memory usage at all. Well done 👍

Tested on
MacOS High Sierra
Intel Core i7
16 GB RAM
SDK: 3.0.100-alpha1-009708

@FrankDoersam

This comment has been minimized.

Copy link

FrankDoersam commented Nov 5, 2018

Thank you for the info. However, after unloading, the DLL could not be deleted from the directory? Have I possibly ignored something?

Thanks in advance.
Yours sincerely
Frank Dörsam

Code:
class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
{
public SimpleUnloadableAssemblyLoadContext()
: base(isCollectible: true)
{
}

protected override Assembly Load(AssemblyName assemblyName) => null;

}

class Program
{
private static void ExecuteAssembly(Assembly assembly)
{
MethodInfo entry = assembly.EntryPoint;
foreach (Type type in assembly.GetTypes())
{
Console.WriteLine(type.FullName);
}
Console.WriteLine("ok");

}

static void Main(string[] args)
{
    AssemblyLoadContext tt = new SimpleUnloadableAssemblyLoadContext();
    tt.LoadFromAssemblyPath(@"C:\Lokale Daten\AppDomainUnload\ConsoleApp1\bin\Debug\netcoreapp3.0\Plugin\Plugin1.dll");

    tt.Unload();
    Console.ReadKey();
}

private static void Context_Unloading(AssemblyLoadContext obj)
{
    Console.WriteLine("Unloading");
}

}

@janvorli

This comment has been minimized.

Copy link
Member

janvorli commented Nov 5, 2018

@FrankDoersam calling "Unload" just initiates the unloading. The actual collection happens after one or more (depending on whether your code that's loaded into the assembly load context uses finalizers). Also, you have to make sure there are no GC references to your SimpleUnloadableAssemblyLoadContext or anything that lives inside of that context. In your code, the tt holds the reference possibly until the end of the Main.
So to make it work reliably, you'd need to:

  • put all the stuff that deals with the context into a separate function marked using the [MethodImpl(MethodImplOptions.NoInlining)] attribute. This ensures that no reference to the assembly load context can leak into the Main.
  • After that function exits, call GC.Collect(); GC.WaitForPendingFinalizers(); . As I've mentioned above, you may need to do that multiple times if the code running inside of the context has finalizers.

To be sure that the assembly load context was unloaded, you can return WeakReference containing the AssemblyLoadContext from the non-inlineable function and loop the GC.Collect(); GC.WaitForPendingFinalizers(); until the weak reference's IsAlive returns false. You can limit the number of cycles to some number and if you make more loops than that, you can consider the unload failed. This could happen in case you still have some reference left.

@FrankDoersam

This comment has been minimized.

Copy link

FrankDoersam commented Nov 5, 2018

Thank you for the helpful tip. Could you please show a short example code that implements the function. (Unfortunately there is very little currently available on the topic, I think the example would be very helpful for many :))

Thanks in advance.

@janvorli

This comment has been minimized.

Copy link
Member

janvorli commented Nov 5, 2018

@FrankDoersam here is your test modified as per my tips:

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

    protected override Assembly Load(AssemblyName assemblyName) => null;
}

class Program
{
    private static int ExecuteAssembly(Assembly assembly, string[] args)
    {
        MethodInfo entry = assembly.EntryPoint;

        object result = entry.GetParameters().Length > 0 ?
                    entry.Invoke(null, new object[] { args }) :
                    entry.Invoke(null, null);

        return (result != null) ? (int)result : 0;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static WeakReference LoadInContextAndUnload()
    {
        AssemblyLoadContext tt = new SimpleUnloadableAssemblyLoadContext();
        Assembly assembly = tt.LoadFromAssemblyPath(@"C:\Lokale Daten\AppDomainUnload\ConsoleApp1\bin\Debug\netcoreapp3.0\Plugin\Plugin1.dll");

        ExecuteAssembly(assembly, new string[] { "arg1", "arg2"}); 

        tt.Unload();

        return new WeakReference(tt)
    }
    static void Main(string[] args)
    {
        WeakReference ttWeakRef = LoadInContextAndUnload();

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

        if (ttWeakRef.IsAlive)
        {
            Console.WriteLine("Unload failed");
        }

        Console.ReadKey();
    }

    private static void Context_Unloading(AssemblyLoadContext obj)
    {
        Console.WriteLine("Unloading");
    }
}
@per-samuelsson

This comment has been minimized.

Copy link

per-samuelsson commented Nov 6, 2018

@FrankDoersam here is your test modified as per my tips

Might be an idea to hide this complexity before going stable. Or I guess you'll risk seeing quite some issues now and then on your trackers. Just saying. 😎

WTBS, thanks for the sample.

@FrankDoersam

This comment has been minimized.

Copy link

FrankDoersam commented Nov 6, 2018

Would be nice if you could extend the function Unload to this? :)

@pixeltris

This comment has been minimized.

Copy link

pixeltris commented Nov 7, 2018

I agree it would be nice if .Unload were to unload immediately as it does with AppDomain.Unload. Though if this means .Unload would have to be changed to invoke a full GC internally it would perhaps be better as an optional thing.

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Nov 7, 2018

AppDomain.Unload does not release the assemblies immediately, The assemblies are still loaded in memory when AppDomain.Unload returns. They are released later once full GC runs and the rest of the AppDomain tear down sequence finishes in the background.

AssemblyLoadContext.Unload behavior is same in this regard.

@uffebjorklund

This comment has been minimized.

Copy link

uffebjorklund commented Nov 7, 2018

Any differences between Windows and MacOS (for example)?
I have no issues running my simple sample below that loads, runs, unloads and copy and then delete the assembly 10000 times. @FrankDoersam @jkotas @janvorli

Maybe this works without issues because of the simple nature of the assembly I load?
Just a simple Hello World application.

namespace Foo
{
    using System.IO;
    using System.Reflection;
    using System.Runtime.Loader;
    using System;

    // Custom ACL
    class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
    {
        public SimpleUnloadableAssemblyLoadContext() : base(isCollectible: true) {}

        protected override Assembly Load(AssemblyName assemblyName) => null;
    }

    class Program
    {
        private static int ExecuteAssembly(Assembly assembly, string[] args)
        {
            MethodInfo entry = assembly.EntryPoint;

            object result = entry.GetParameters().Length > 0 ?
                entry.Invoke(null, new object[] { args }) :
                entry.Invoke(null, null);

            return (result != null) ? (int) result : 0;
        }

        // Pre Req: A folder in the root named `F0` that contains the assembly to load and execute.
        static void Main(string[] args)
        {
            Console.WriteLine("Starting...");
            for (var i = 0; i < 10000; i++)
            {
                var context = new SimpleUnloadableAssemblyLoadContext();
                var stream = GetAssemblyStream(i);
                Assembly assembly = context.LoadFromStream(stream);
                stream.Dispose();
                ExecuteAssembly(assembly, Array.Empty<string>());
                context.Unload();
                CopyAndDelete(i);
            }

            Console.WriteLine("Done");
            Console.ReadLine();
        }

        private static Stream GetAssemblyStream(int i)
        {
            return System.IO.File.OpenRead($"./F{i.ToString()}/hello.dll");
        }

        private static void CopyAndDelete(int i)
        {
            try
            {
                var dir1 = $"./F{i.ToString()}";
                var dir2 = $"./F{(i + 1).ToString()}";
                System.IO.Directory.CreateDirectory(dir2);
                System.IO.File.Copy($"{dir1}/hello.dll", $"{dir2}/hello.dll");
                // To keep to original F0 folder
                if (i > 0)
                {
                    System.IO.Directory.Delete(dir1, true);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Failed to move and delete file => {ex.Message}");
            }
        }
    }
}
@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Nov 7, 2018

Maybe this works without issues because of the simple nature of the assembly I load?

It works without issue because of you are creating a copy of the whole assembly upfront. It avoids locking the files on disk, but you pay performance penalty for it.

@uffebjorklund

This comment has been minimized.

Copy link

uffebjorklund commented Nov 7, 2018

@jkotas I understand that, I was actually just testing to delete the assembly that was unloaded since @FrankDoersam had issues with deleting the assembly after unloading.

Edit: I had no issues deleting before I did the copy thing either

Learning a lot of tricks in this thread :) Will be of great use to us when 3.0 is released

@pixeltris

This comment has been minimized.

Copy link

pixeltris commented Nov 10, 2018

I have a few questions:

  1. In the normal AppDomain loading/unloading situation LoadFrom allows for shadow copying. Will there be any support for shadow copying with AssemblyLoadContext? It is helpful in situations where you want to modify a loaded dll and reload it when modified.

  2. Calls to AppDomain.CurrentDomain.GetAssemblies() / Assembly.LoadFrom() can be undesirable when trying to create self contained contexts. Is my only option hooking those functions and log warnings when used?

@bitbonk

This comment has been minimized.

Copy link

bitbonk commented Nov 10, 2018

@jkotas @janvorli How do I know when an assembly has actually been unloaded. Can I poll it or get notified about it somehow?

@poizan42

This comment has been minimized.

Copy link
Collaborator

poizan42 commented Nov 10, 2018

@bitbonk From @janvorli's example earlier, when your custom AssemblyLoadContext gets collected by the GC - so you can either poll it by using a WeakReference as in his example or get a callback by adding a finalizer to it.

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Nov 10, 2018

Will there be any support for shadow copying with AssemblyLoadContext?

We do not plan to have built-in support for shadow copying in the runtime. You can implement it yourself using AssemblyLoadContext with any policy you like (where to copy, how much to copy, how to copy it, when to delete, ...). .NET Framework had one hard-coded policy for shadow copying and we always got an endless stream of request how to make it more configurable.

Alternatively, you can avoid locking files in memory by loading the assembly bits into memory first, and then hand that memory to AssemblyLoadContext.

AppDomain.CurrentDomain.GetAssemblies() / Assembly.LoadFrom() logging

We plan to have better tracing for assembly loader in general. Yes, these are poor APIs that should not be used by well-written programs (even if you do not create self contained contexts).

@pixeltris

This comment has been minimized.

Copy link

pixeltris commented Nov 10, 2018

You can implement it yourself using AssemblyLoadContext

One nice feature of shadow copying is that it sets up Assembly.CodeBase pointing to the original file location. Is there a way to assign this manually? If not I think it would be nice if there was an overload for it in LoadFromAssemblyPath().

Also things like subscribed global events in non-collectible assemblies can keep a context alive. Is there any easy way of finding all objects which are keeping the context alive?

@jkotas

This comment has been minimized.

Copy link
Member

jkotas commented Nov 10, 2018

Assembly.CodeBase

We would like to be able to enable linking of .NET Core applications into single file (dotnet/coreclr#20287). Code that depends on properties like this always breaks "single file". I do not think we would want to be adding more APIs that make it easier for more code to depend on physical locations of .dlls on disk.

Is there any easy way of finding all objects which are keeping the context alive?

It is no different from finding all object which are keeping other objects alive: how to find memory leak in C#.

@pixeltris

This comment has been minimized.

Copy link

pixeltris commented Nov 10, 2018

@jkotas makes sense, thanks for the help.

If anybody is interested I wrote something which lets me create a collectible AssemblyLoadContext at runtime using System.Reflection.Emit to support assembly unloading under CoreCLR without having depending on anything .NET Core related (I'm sure just creating an extra project with some #if directives would be much more sane). I created a small interface to suit my needs and the IL code gen is here. It is a hacky mess to suit my needs but it may be useful to someone as a starting point for something nicer. Also don't look at anything else in that file (even more hacky mess!).

@bitbonk

This comment has been minimized.

Copy link

bitbonk commented Nov 10, 2018

@bitbonk From @janvorli's example earlier, when your custom AssemblyLoadContext gets collected by the GC - so you can either poll it by using a WeakReference as in his example or get a callback by adding a finalizer to it.

I am not sure if there is a safe way to build a notification mechanism using finalizers. AFAIK you should never access managed references in a finalizer because they may have already been finalized. So all I can do is polling I guess.

Also WeakReferences may be expensive since the allocate a GC Handle. We've been bitten my memory leaks caused by WeakReferences and their GC handles.

@poizan42

This comment has been minimized.

Copy link
Collaborator

poizan42 commented Nov 11, 2018

AFAIK you should never access managed references in a finalizer because they may have already been finalized.

If whatever component you are calling into is designed properly you should get an ObjectDisposedException if that is the case which you can just choose to swallow. But ofc. if you actually want a callback then it is your responsibility to keep that object alive some other way independently of the AssemblyLoadContext.

And just to be clear here, collected != finalized. Every object accessible from the object which finalizer you are in must still be live even if they may have been finalized and are currently marked for collection - you can also still resurrect them by making them rooted again. You are not touching freed memory, they are still valid .NET objects.

@Diaskhan

This comment has been minimized.

Copy link

Diaskhan commented Dec 6, 2018

What about docs ? Any plans to desribe a usage of unlodable assemblies ?

I guesse its gonna killer feature when released! Because net core gona be more modular ! And it gonna be easy to extend functionality of software ! Cms! All cases when modularity is aproved !

@janvorli

This comment has been minimized.

Copy link
Member

janvorli commented Dec 6, 2018

@Diaskhan I am planning to write doc on that with examples, hints etc. so that people can successfully use this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment