Search code examples
c#wpfasync-awaitdispatcher

How to switch task to a WPF Dispatcher with a priority?


WPF Dispatcher has a method InvokeAsync that accepts an Action or a Func<T> and DispatcherPriority.

I need to pass Func<Task<T>> and I need to run whole Task<T> on a WPF Dispatcher with a given priority.

Example:

I need to call an async function Foo that has to run on Dispatcher thread:

async Task<SomeResult> Foo()
{
    // some view model modification that needs to run on UI thread
    ...

    // calling some IO call on thread pool
    await Task.Run(() => SomeIoCallAsync()); 

    // more view model modifications
    ...

    // calling something on thread pool
    await Task.Run(() => HeavyComputation());

    // more view model modifications
    ...

    return someResult;
}

from a async function Bar that is not running on UI thread:

async Task<SomeOtherResult> Bar()
{
    ...

    TODO call Foo() on UI thread

    ...

}

Note: If Foo would not be an async function but just synchronous, Bar could look like this:

async Task<SomeOtherResult> Bar()
{
    ...

    await dispatcher.InvokeAsync(Foo, someDispatcherPriority);

    ...

}

How can I call async function on a dispatcher thread with a some priority?


Solution

  • I have found the answer in the source code. The actual behavior has changed in .NET Framework 4.5. The comment states:

    WPF <= 4.0 a DispatcherSynchronizationContext always used DispatcherPriority.Normal to satisfy SynchronizationContext.Post and SynchronizationContext.Send calls.

    With the inclusion of async Task-oriented programming in .Net 4.5, we now record the priority of the DispatcherOperation in the DispatcherSynchronizationContext and use that to satisfy SynchronizationContext.Post and SynchronizationContext.Send calls. This enables async operations to "resume" after an await statement at the same priority they are currently running at.

    This is, of course, an observable change in behavior.

    Test code:

    void Main()
    {
        var d = Dispatcher.CurrentDispatcher;
        LogThread("Begin");
        
        d.InvokeAsync(async () =>
        {
            LogThread("Main #1");
    
            await Task.Run(async () => 
            {
                LogThread("[WT] Main Task #1");
                var t = await d.InvokeAsync(Foo, DispatcherPriority.ApplicationIdle);
                for (var i = 0; i < 10; i++)
                {
                    var a = i;
                    d.BeginInvoke(() =>
                    {
                        LogThread($"Actions {a}");
                        Thread.Sleep(100);
                    }, DispatcherPriority.Background);
                }
                LogThread("[WT] Main Task #2");
                await t;
                LogThread("[WT] Main Task #3");
            });
            LogThread("Main #2");
    
            d.InvokeShutdown();
        }, DispatcherPriority.Normal);
        
        
        Dispatcher.Run();
        
        LogThread("End");
    }
    
    async Task Foo() 
    {
        LogThread("Foo #1");
        await Task.Run(() =>
        {
            LogThread("[WT] Foo #2 Sleep");
            Thread.Sleep(100);
            LogThread("[WT] Foo #3 Sleep");
        });
        LogThread("Foo #4");
        await Task.Run(async () =>
        {
            LogThread("[WT] Foo #5 Delay");
            await Task.Delay(100);
            LogThread("[WT] Foo #6 Delay");
        });
        LogThread("Foo #5");
    }
    
    void LogThread(string id) => Console.WriteLine($"{Environment.CurrentManagedThreadId} {GetPriority()} - {id}");
    
    object? GetPriority()
    {
        var dsc = SynchronizationContext.Current as DispatcherSynchronizationContext;
        if (dsc is null)
            return "WT";
        return typeof(DispatcherSynchronizationContext).GetField("_priority", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(dsc);
    }
    

    Output:

    1 WT - Begin
    1 Normal - Main #1
    11 WT - [WT] Main Task #1
    1 ApplicationIdle - Foo #1
    11 WT - [WT] Foo #2 Sleep
    10 WT - [WT] Main Task #2
    1 Background - Actions 0
    11 WT - [WT] Foo #3 Sleep
    1 Background - Actions 1
    1 Background - Actions 2
    1 Background - Actions 3
    1 Background - Actions 4
    1 Background - Actions 5
    1 Background - Actions 6
    1 Background - Actions 7
    1 Background - Actions 8
    1 Background - Actions 9
    1 ApplicationIdle - Foo #4
    8 WT - [WT] Foo #5 Delay
    11 WT - [WT] Foo #6 Delay
    1 ApplicationIdle - Foo #5
    11 WT - [WT] Main Task #3
    1 Normal - Main #2
    1 WT - End