简体   繁体   中英

Memory leaks in .NET when doing async over sync

I have a situation where I must call an async method synchronously, and it is done so as follows:

 obj.asyncMethod().Wait(myCancelToken)

If the cancellation token is switched the disposable's within the task will not get disposed despite being activated via a using statement.

The below program illustrates the problem:

        using System;
        using System.Threading;
        using System.Threading.Tasks;

        namespace LeakTest {

            class Program {
                static void Main(string[] args) {
                    try {
                        var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
                        LongRunningTask().Wait(timeout.Token);
                    } catch (OperationCanceledException error) {                
                        //  handling timeout is logically okay, but expect nothing to be leaked
                    }
                    Console.WriteLine("Leaked Instances = {0}", DisposableResource.Instances);
                    Console.ReadKey();
                }

                static async Task LongRunningTask() {
                    using (var resource = new DisposableResource()) {
                        await Task.Run( () => Thread.Sleep(1000));
                    }
                }

                public class DisposableResource : IDisposable {
                    public static int Instances = 0;
                    public DisposableResource() {
                        Instances++;
                    }

                    public void Dispose() {
                        Instances--;
                    }
                }
            }
        }

It seems Wait method just kills the task thread on cancellation instead of triggering an exception within that thread and letting it terminate naturally. Question is why?

You've cancelled the task returned by Wait(timeout.Token) not the one returned from LongRunningTask , if you want to cancel that one pass the token to Task.Run and also use await Task.Delay instead of Thread.Sleep and pass the token there as well.

static void Main(string[] args)
{
    try
    {
        var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        LongRunningTask(timeout.Token).Wait();
    }
    catch (AggregateException error)
    {
        //  handling timeout is logically okay, but expect nothing to be leaked
    }
    Console.WriteLine("Leaked Instances = {0}", DisposableResource.Instances);
    Console.ReadLine();
}

static async Task LongRunningTask(CancellationToken token)
{

    using (var resource = new DisposableResource())
    {
        await Task.Run(async () => await Task.Delay(1000, token), token);
    }
}

public class DisposableResource : IDisposable
{
    public static int Instances = 0;
    public DisposableResource()
    {
        Instances++;
    }

    public void Dispose()
    {
        Instances--;
    }
}

Note that the using statment will still dispose of the resource once the long running operation finishes. Run this example:

static void Main(string[] args)
{
    try {
            var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
            LongRunningTask().Wait(timeout.Token);
        } catch (OperationCanceledException error) {                
            //  handling timeout is logically okay, but expect nothing to be leaked
        }

    Console.WriteLine("Leaked Instances = {0}", DisposableResource.Instances);

    Console.ReadKey();
}

static async Task LongRunningTask()
{
    using (var resource = new DisposableResource())
    {
        await Task.Run(() => Thread.Sleep(1000));
    }
}

public class DisposableResource : IDisposable
{
    public static int Instances = 0;
    public DisposableResource()
    {
        Instances++;
    }

    public void Dispose()
    {
        Instances--;
        Console.WriteLine("Disposed resource. Leaked Instances = {0}", Instances);
    }
}

Output
Leaked Instances = 1
Disposed resource. Leaked Instances = 0

It seems Wait method just kills the task thread on cancellation instead of triggering an exception within that thread

You are incorrect, on when you cancel the only thing that happens is you stop waiting for Wait(myCancelToken) to complete, the task is still running in the background.

In order to cancel the background task you must pass the cancelation token into all of the methods down the chain. If you want the innermost layer (the long running one) to stop early that code must call token.ThrowIfCancellationRequested() throughout its code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM