Skip to content
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 #21707

Closed
bartstinson opened this issue May 15, 2017 · 52 comments
Closed

Unload assembly from runtime #21707

bartstinson opened this issue May 15, 2017 · 52 comments
Labels
area-System.Runtime question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@bartstinson
Copy link

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
Copy link
Member

You can't right now AFAIK.

@danmoseley
Copy link
Member

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

@bartstinson what are you trying to achieve?

@bartstinson
Copy link
Author

@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
Copy link
Member

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

@jkotas
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. https://github.com/dotnet/coreclr/issues/552 is the proposed plan to add it.

@bartstinson
Copy link
Author

@davidfowl dll has fixed name.

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

@jkotas
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
Copy link
Author

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

@craigajohnson
Copy link
Contributor

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
Copy link
Contributor

poizan42 commented Nov 14, 2017

@agatlin
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
Copy link
Contributor

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
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
Copy link

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
Copy link

seriouz commented Mar 30, 2018

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

@bugproof
Copy link

Why is this closed anyway?

@MgSam
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
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
Copy link

@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
Copy link
Member

jkotas commented Oct 30, 2018

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

@jkotas
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
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
Copy link

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
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
Copy link

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
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
Copy link

@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
Copy link

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

@pixeltris
Copy link

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
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
Copy link

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
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
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
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
Copy link
Contributor

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
Copy link
Contributor

@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
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
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
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 (https://github.com/dotnet/coreclr/issues/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
Copy link

@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
Copy link
Contributor

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
Copy link
Contributor

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
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
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.

@Kein
Copy link

Kein commented Jan 29, 2019

Hey, just a small question if you dont mind - I see all the issues in Unloadability project
https://github.com/dotnet/coreclr/projects/9
target Milestone 3.0 - is this still up to date and actual or the project not gonna make its way into the 3.0?

@jkotas
Copy link
Member

jkotas commented Jan 29, 2019

It is on track for .NET Core 3.0. It has been mentioned in the official .NET Core 3.0 Preview 2 blog post: https://blogs.msdn.microsoft.com/dotnet/2019/01/29/announcing-net-core-3-preview-2/

@TETYYS
Copy link

TETYYS commented Feb 1, 2019

Any tips on reducing memory usage? Right now one AssemblyLoadContext + Assembly takes up about 8MB, but I wish to load more than two hundred unloadable assemblies in a pretty limited environment, it adds up quickly.

@jkotas
Copy link
Member

jkotas commented Feb 2, 2019

@TETYYS 8MB for one AssemblyLoadContext with one typical assembly loaded is not expected. You may be loading second copy of all assemblies into your assembly context.

https://github.com/dotnet/samples/tree/master/core/tutorials/Unloading has a sample of a simple unloadable plugin. It costs less than 100kB on average to load the plugin if the sample is modified to load the plugin a loop.

cc @janvorli

@TETYYS
Copy link

TETYYS commented Feb 2, 2019

Thanks, I'm not sure what exactly I did worked, but I think fiddling with dependencies of assembly did it. Memory is no longer an issue even in limited environment with more than 1000 assemblies. Thanks again for your hard work.

@danmoseley
Copy link
Member

If you can figure out what your were doing wrong, it might be useful to share here as an antipattern for others to see.

@vasilijgla
Copy link

vasilijgla commented Mar 7, 2019

How about unhandled exceptions in plugin? it is real catch it on host level? (legacy .net allows it in a buggy way
appDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
)

@janvorli
Copy link
Member

janvorli commented Mar 7, 2019

@vasilijgla yes, it works just fine. Both try / catch around the call to a plugin method and AppDomain.UnhandledException work.

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 2.0.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 23, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Runtime question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests