Synchronous or asynchronous continuation upon TaskCompletionSource.TrySetResult?

async-await, asynchronous, c#, multithreading, task-parallel-library

Solution

I don't think there is a documented way to predict synchronous/asynchronous behaviour of `SetResult` in advance. If you want to explicitly impose asynchronous continuation, the `Task.Run(() => tcs.SetResult())` idea proposed by @Damien_The_Unbeliever is simple and universal.

However, if you really would like to reduce thread switching and still force asynchrony, you could wrap `tcs.SetResult` with a custom dumb `SynchronizationContext`. The only purpose of it would be its uniqueness as compared to the context of `await tcs.Task` (and to that of any other continuations possibly registered on `tcs.Task`). That would cause asynchronous continuation(s) on the consumer side of `TaskCompletionSource`, either via `SynchronizationContext.Post` or on a pool thread (if there's no synchronization context on the consumer side).

The test app:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WinForms_21845495
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();

            this.Load += async (s, e) =>
            {
                // test on WindowsFormsSynchronizationContext
                await RequestNotificationAsync(notifyAsync: false);
                Debug.WriteLine(String.Empty);

                await RequestNotificationAsync(notifyAsync: true);
                Debug.WriteLine(String.Empty);

                // test on a pool thread
                await Task.Run(() => RequestNotificationAsync(notifyAsync: false));
                Debug.WriteLine(String.Empty);

                await Task.Run(() => RequestNotificationAsync(notifyAsync: true));
                Debug.WriteLine(String.Empty);
            };
        }

        async Task RegisterNotification(TaskCompletionSource<object> tcs, bool notifyAsync)
        {
            await Task.Delay(500);

            Debug.WriteLine("A.before");

            if (notifyAsync)
            {
                tcs.SetResultAsync(null);
            }
            else
            {
                tcs.SetResult(null);
            }

            Debug.WriteLine("A.after");
        }

        async Task RequestNotificationAsync(bool notifyAsync)
        {
            var tcs = new TaskCompletionSource<object>();
            var task = this.RegisterNotification(tcs, notifyAsync);

            Debug.WriteLine("B.before");

            var data = await tcs.Task;

            // do not yeild
            Thread.Sleep(500); 
            Debug.WriteLine("B.after");

            // yeild
            await Task.Delay(500); 
        }
    }

    public static class TaskExt
    {
        static public void SetResultAsync<T>(this TaskCompletionSource<T> tcs, T result)
        {
            FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
        }

        // FakeSynchronizationContext
        class FakeSynchronizationContext : SynchronizationContext
        {
            private static readonly ThreadLocal<FakeSynchronizationContext> s_context =
                new ThreadLocal<FakeSynchronizationContext>(() => new FakeSynchronizationContext());

            private FakeSynchronizationContext() { }

            public static FakeSynchronizationContext Instance { get { return s_context.Value; } }

            public static void Execute(Action action)
            {
                var savedContext = SynchronizationContext.Current;
                SynchronizationContext.SetSynchronizationContext(FakeSynchronizationContext.Instance);
                try
                {
                    action();
                }
                finally
                {
                    SynchronizationContext.SetSynchronizationContext(savedContext);
                }
            }

            // SynchronizationContext methods

            public override SynchronizationContext CreateCopy()
            {
                return this;
            }

            public override void OperationStarted()
            {
                throw new NotImplementedException("OperationStarted");
            }

            public override void OperationCompleted()
            {
                throw new NotImplementedException("OperationCompleted");
            }

            public override void Post(SendOrPostCallback d, object state)
            {
                throw new NotImplementedException("Post");
            }

            public override void Send(SendOrPostCallback d, object state)
            {
                throw new NotImplementedException("Send");
            }
        }
    }
}

The output:

B.before
A.before
B.after
A.after

B.before
A.before
A.after
B.after

B.before
A.before
B.after
A.after

B.before
A.before
A.after
B.after

Problem

How to tell if the continuation initiated by `TaskCompletionSource.TrySetResult` is going to be executed synchronously or asynchronously? For example: ``` // class A void RegisterNotification(TaskCompletionSource<object> tcs) { this.source.NotificationEvent += (s, eData) => { Debug.WriteLine("A.before"); tcs.TrySetResult(eData.Result); Debug.WriteLine("A.after"); DoProcessingA(); }; } // class B async Task RequestNotificationAsync() { var tcs = new TaskCompletionSource<object>(); this.a.RegisterNotification(tcs); Debug.WriteLine("B.before"); var data = await tcs.Task; Debug.WriteLine("B.after"); DoProcessingB(); } ``` If `NotificationEvent` is fired on a thread with the different synchronization context from that of where `await tcs.Task` took place, the debug output will be: ``` B.before A.before A.after B.after ``` That is, the `await tcs.Task` continuation is executed asynchronously. If it is fired on the same synchronization context (or if there is no synchronization context in both places), the output will be: ``` B.before A.before B.after A.after ``` That is, the continuation is executed synchronously. Is there a way to predict this order inside `RegisterNotification`? I could save `SynchronizationContext.Current` inside `RegisterNotification` and compare it later when I call `tcs.TrySetResult`. But that not necessarily mean that `await tcs.Task` will take place on the context I saved. In theory, if I could predict this, I might be able to use it to diagnose and prevent potential deadlocks.

Original source