8

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?

Dusan
  • 5,000
  • 6
  • 41
  • 58
  • "The window is launched in separate thread" - why? You should never need that, and async/await made avoiding it easier. – H H Apr 02 '18 at 09:12
  • @HenkHolterman Because users needs to be able to work with separate functionalities without each one blocking all the others with modal dialogs. – Dusan Apr 02 '18 at 09:15
  • So you use threads to revert `.ShowDialog()` to `.Show()` ? – H H Apr 02 '18 at 09:17
  • Think that real issue in here is - what happens to continuation of the awaited task when it cannot resume because the original thread/sync context has ceased to exists? – Dusan Apr 02 '18 at 09:18
  • @HenkHolterman No, think of each thread as a standalone application... You can launch some functionality in separate thread, and everything that happens in there lives life on it's own, does not block other launched functionalities or the main thread. So, when using separate threads, user can multitask in much simpler way - for example, if I open something as modal in one thread, it does not block entire application, just that single thread/functionality. It is about usability, mate. Same as, for example, web browsers do not block all the tabs if one of them opens the js alert. – Dusan Apr 02 '18 at 09:26
  • Do you have try-catch block in `LoadData` or anywhere up? Did you ensure it's not being triggered in this case? I'd expect that task should end up in cancelled state. – Evk Apr 02 '18 at 09:30
  • 1
    Your first mistake here is to create another sta thread and use that for any UI. Don't do that. It's a really bad idea which causes way more problems that it's worth. – Andy Apr 02 '18 at 09:31
  • @Evk I have put a simplified version of the code. There is a try catch block in there, event the cancellation token is in there. I also trigger the cancellation on window close. However, nor the cancellation is done, nor the cancellation exception has been caught. – Dusan Apr 02 '18 at 09:32

1 Answers1

8

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

Evk
  • 98,527
  • 8
  • 141
  • 191
  • I haven't verified that but I'd expect `TaskScheduler.UnobservedTaskException` to be fired in this scenario for failed continuations, although not before GC takes place. I wrote more on this [here](https://stackoverflow.com/a/22395161/1768303). – noseratio Apr 03 '18 at 13:50
  • @Noseratio I don't think so. In this case no exceptions are thrown, just continuation of async method is not executed (ignored by shutdown dispatcher). So I expect target task to be in running state. In example by your link task is faulted but just not observed. – Evk Apr 03 '18 at 14:08