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 -> ...
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?
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With