How can I prevent synchronous continuations on a Task?

后端 未结 6 463
佛祖请我去吃肉
佛祖请我去吃肉 2020-11-27 11:04

I have some library (socket networking) code that provides a Task-based API for pending responses to requests, based on TaskCompletionSource

6条回答
  •  情歌与酒
    2020-11-27 11:25

    Updated, I posted a separate answer to deal with ContinueWith as opposed to await (because ContinueWith doesn't care about the current synchronization context).

    You could use a dumb synchronization context to impose asynchrony upon continuation triggered by calling SetResult/SetCancelled/SetException on TaskCompletionSource. I believe the current synchronization context (at the point of await tcs.Task) is the criteria TPL uses to decide whether to make such continuation synchronous or asynchronous.

    The following works for me:

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

    SetResultAsync is implemented like this:

    public static class TaskExt
    {
        static public void SetResultAsync(this TaskCompletionSource tcs, T result)
        {
            FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
        }
    
        // FakeSynchronizationContext
        class FakeSynchronizationContext : SynchronizationContext
        {
            private static readonly ThreadLocal s_context =
                new ThreadLocal(() => 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");
            }
        }
    }
    

    SynchronizationContext.SetSynchronizationContext is very cheap in terms of the overhead it adds. In fact, a very similar approach is taken by the implementation of WPF Dispatcher.BeginInvoke.

    TPL compares the target synchronization context at the point of await to that of the point of tcs.SetResult. If the synchronization context is the same (or there is no synchronization context at both places), the continuation is called directly, synchronously. Otherwise, it's queued using SynchronizationContext.Post on the target synchronization context, i.e., the normal await behavior. What this approach does is always impose the SynchronizationContext.Post behavior (or a pool thread continuation if there's no target synchronization context).

    Updated, this won't work for task.ContinueWith, because ContinueWith doesn't care about the current synchronization context. It however works for await task (fiddle). It also does work for await task.ConfigureAwait(false).

    OTOH, this approach works for ContinueWith.

提交回复
热议问题