Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task await and thread terminate are causing memory leak

I am executing the async Task in my window:

private async Task LoadData()
{
    // Fetch data
    var data = await FetchData();

    // Display data
    // ...
}

The window is launched in separate thread:

// Create and run new thread
var thread = new Thread(ThreadMain);
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.CurrentCulture = Thread.CurrentThread.CurrentCulture;
thread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture;
thread.Start();

private static void ThreadMain()
{
    // Set synchronization context
    var dispatcher = Dispatcher.CurrentDispatcher;
    SynchronizationContext.SetSynchronizationContext(
        new DispatcherSynchronizationContext(dispatcher));

    // Show window
    var window = new MyWindow();
    window.ShowDialog();

    // Shutdown
    dispatcher.InvokeShutdown();
}

When the windows is closed, the thread is of course finished, which is just fine.

Now - if that happens before the FetchData is finished, I get the memory leak.

Appears that FetchData remains awaited forever and the Task.s_currentActiveTasks static field is retaining my window instance forever:

Retention path

System.Collections.Generic.Dictionary.entries -> System.Collections.Generic.Dictionary+Entry[37] at [17].value -> System.Threading.Tasks.Task.m_continuationObject -> System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.m_action -> System.Action._target -> System.Runtime.CompilerServices.AsyncMethodBuilderCore+ContinuationWrapper.m_continuation -> System.Action._target -> System.Runtime.CompilerServices.AsyncMethodBuilderCore+ContinuationWrapper.m_continuation -> System.Action._target -> ...

dotMemory sample

If I understand this correctly, if/when the FetchData completes, the continuation should continue on the window instance target and a thread, but that never happens since the thread is finished.

Is there any solutions to this, how to avoid memory leak in this case?

like image 851
Dusan Avatar asked Apr 02 '18 08:04

Dusan


1 Answers

I think there is not much you can do to fix this (I mean without changing overall design). With await and available SynchronizationContext, continuation (after await) is posted to that context. This continuation includes code which completes resulting task. So in your example:

private async Task LoadData()
{
    var data = await FetchData();
    // the rest is posted to sync context
    // and only when the rest is finished
    // task returned from LoadData goes to completed state         
} 

Posting to WPF sync context is the same as doing BeginInvoke on dispatcher. However, doing BeginInvoke on dispatcher which has shutdown throws no exceptions and returns DispatcherOperation with status of DispatcherOperationStatus.Aborted. Of course, delegate you passed to BeginInvoke is not executed in this case.

So in result - continuation of your LoadData is passed to shutdown dispatcher which silently ignores it (well, not silently - it returns Aborted state, but there is no way to observe it, because SynchronizationContext.Post has return type void). Since continuation is never executed - task returned from LoadData never completes\fails\cancels and is always in running state.

You can verify it by providing your own sync context and see how it goes:

internal class SimpleDispatcherContext : SynchronizationContext
{
    private readonly Dispatcher _dispatcher;
    private readonly DispatcherPriority _priority;
    public SimpleDispatcherContext(Dispatcher dispatcher, DispatcherPriority priority = DispatcherPriority.Normal)
    {
        _dispatcher = dispatcher;
        _priority = priority;
    }

    public override void Post(SendOrPostCallback d, object state) {
        var op = this._dispatcher.BeginInvoke(_priority, d, state);
        // here, default sync context just does nothing
        if (op.Status == DispatcherOperationStatus.Aborted)
            throw new OperationCanceledException("Dispatcher has shut down");
    }

    public override void Send(SendOrPostCallback d, object state) {
        _dispatcher.Invoke(d, _priority, state);
    }
}

private async Task LoadData()
{
    SynchronizationContext.SetSynchronizationContext(new SimpleDispatcherContext(Dispatcher));
    // Fetch data
    var data = await FetchData();

    // Display data
    // ...
}

So you can either live with that (if you consider this a minor leak - because how many windows should user really open for this to have noticable effect, thousands?) or somehow track your pending operations and prevent closing window until they are all resolved (or allow closing window but prevent dispatcher shutdown until all pending operations are resolved).

like image 185
Evk Avatar answered Oct 26 '22 01:10

Evk