8
\$\begingroup\$

Using await task.ConfigureAwait(false) is a standard best practice for indicating to the compiler that the continuation does not need to be run on the current context.

We also have the Task.Yield() method which hands control back to the calling context and always posts a continuation (no fast-path optimizations); however, since this does not return a Task, you cannot use ConfigureAwait(false) and the continuation is always scheduled on the current synchronization context (if there is one).

Without running a task manually, I could not see a standard way of both yielding and ensuring/indicating that the continuation did not require the synchronization context.

I trimmed down the Task.Yield source code to create the TaskEx.YieldToThreadPool method below which returns an awaitable that always schedules its continuation on the TaskPool.

It could be used in any situation where a Yield makes sense and further Tasks use ConfigureAwait(false).

In addition, within non performance critical code, it could be used to ensure that the method is running on the TaskPool without manually spawning a Task, or splitting the method.

There could also be an additional check in the IsCompleted property to see if the current thread is already on the TaskPool and not yield.

I have tested the method and it functions as expected - however I would greatly appreciate the thoughts of the Async-Await gurus. Did I miss a fundamental concept that makes this wrong/unnecessary?

//Example usage:
public async Task MixedAsyncAndCpuBoundMethod()
{
    await TaskEx.YieldToThreadPool();

    //Now on ThreadPool thread, no risk of async-await 'fast-path' skipping-
    //over the continuation. 

    CpuBoundWork();
    Thread.Sleep(1000);

    await AsyncMethod().ConfigureAwait(false);
}

/// <summary>
/// Yields and schedules the continuation on the <see cref="ThreadPool"/>.
/// </summary>
/// <returns><see cref="ToThreadPoolAwaitable"/>.</returns>
public static ToThreadPoolAwaitable YieldToThreadPool()
{
    return new ToThreadPoolAwaitable();
}

public struct ToThreadPoolAwaitable
{
    public ToThreadPoolAwaiter GetAwaiter() { return new ToThreadPoolAwaiter(); }

    [HostProtection(Synchronization = true, ExternalThreading = true)]
    public struct ToThreadPoolAwaiter : ICriticalNotifyCompletion
    {
        /// <summary>Gets whether a yield is not required.</summary>
        /// <remarks>This property is intended for compiler user rather than use directly in code.</remarks>
        public bool IsCompleted => return false; // always yield
        //public bool IsCompleted => return Thread.CurrentThread.IsThreadPoolThread; // Alternatively, only yield if not on ThreadPool.    

        /// <summary>Posts the <paramref name="continuation"/> back to the current context.</summary>
        /// <param name="continuation">The action to invoke asynchronously.</param>
        /// <exception cref="System.ArgumentNullException">The <paramref name="continuation"/> argument is null (Nothing in Visual Basic).</exception>
        [SecuritySafeCritical]
        public void OnCompleted(Action continuation)
        {
            QueueContinuation(continuation, flowContext: true);
        }

        /// <summary>Posts the <paramref name="continuation"/> back to the current context.</summary>
        /// <param name="continuation">The action to invoke asynchronously.</param>
        /// <exception cref="System.ArgumentNullException">The <paramref name="continuation"/> argument is null (Nothing in Visual Basic).</exception>
        [SecurityCritical]
        public void UnsafeOnCompleted(Action continuation)
        {
            QueueContinuation(continuation, flowContext: false);
        }

        /// <summary>Posts the <paramref name="continuation"/> back to the current context.</summary>
        /// <param name="continuation">The action to invoke asynchronously.</param>
        /// <param name="flowContext">true to flow ExecutionContext; false if flowing is not required.</param>
        /// <exception cref="System.ArgumentNullException">The <paramref name="continuation"/> argument is null (Nothing in Visual Basic).</exception>
        [SecurityCritical]
        private static void QueueContinuation(Action continuation, bool flowContext)
        {
            // Validate arguments
            if (continuation == null) throw new ArgumentNullException(nameof(continuation));

            if (flowContext)
            {
                ThreadPool.QueueUserWorkItem(WaitCallbackRunAction, continuation);
            }
            else
            {
                ThreadPool.UnsafeQueueUserWorkItem(WaitCallbackRunAction, continuation);
            }
        }

        /// <summary>WaitCallback that invokes the Action supplied as object state.</summary>
        private static readonly WaitCallback WaitCallbackRunAction = RunAction;

        /// <summary>Runs an Action delegate provided as state.</summary>
        /// <param name="state">The Action delegate to invoke.</param>
        private static void RunAction(object state) { ((Action)state)(); }

        /// <summary>Ends the await operation.</summary>
        public void GetResult() { } // Nop. It exists purely because the compiler pattern demands it.
    }
}
\$\endgroup\$
4
+50
\$\begingroup\$

First: this is a pretty tidy implementation and there is not much to say from the code review perspective.

Now about the question of necessity: Microsoft did include SwitchTo extension methods in the async CTP but decided to remove them because they were open to misuse. This doesn't mean something like this isn't useful but Microsoft has to consider a much wider audience and an innocuous SwitchTo call in the wrong place could easily lead to weird behaviors and random, hard to track down bugs causing potentially a fair amount of support work so they decided to not ship it (and also because it's reasonable easy to create yourself if you need it).

So essentially if you are careful to avoid using it within your own projects in potentially harmful places (like inside catch and finally blocks) this might be a nice tool in your library to have.

\$\endgroup\$
  • \$\begingroup\$ Thank you kindly for the information and some additional google fodder. I was able to drum-up a number of links regarding SwitchTo including this one which has some further examples. I believe the real danger was in the possibility of code relying on switching back to a context (Dispatcher->TaskPool->Dispatcher). In my usage I would treat the context switch as a one time deal, much like ConfigureAwait(false). \$\endgroup\$ – Andrew Hanlon Nov 12 '15 at 21:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.