19

It's recommended that one use ConfigureAwait(false) whenever when you can, especially in libraries because it can help avoid deadlocks and improve performance.

I have written a library that makes heavy use of async (accesses web services for a DB). The users of the library were getting a deadlock and after much painful debugging and tinkering I tracked it down to the single use of await Task.Yield(). Everywhere else that I have an await, I use .ConfigureAwait(false), however that is not supported on Task.Yield().

What is the recommended solution for situations where one needs the equivalent of Task.Yield().ConfigureAwait(false)?

I've read about how there was a SwitchTo method that was removed. I can see why that could be dangerous, but why is there no equivalent of Task.Yield().ConfigureAwait(false)?

Edit:

To provide further context for my question, here is some code. I am implementing an open source library for accessing DynamoDB (a distributed database as a service from AWS) that supports async. A number of operations return IAsyncEnumerable<T> as provided by the IX-Async library. That library doesn't provide a good way of generating async enumerables from data sources that provide rows in "chunks" i.e. each async request returns many items. So I have my own generic type for this. The library supports a read ahead option allowing the user to specify how much data should be requested ahead of when it is actually needed by a call to MoveNext().

Basically, how this works is that I make requests for chunks by calling GetMore() and passing along state between these. I put those tasks in a chunks queue and dequeue them and turn them into actual results that I put in a separate queue. The NextChunk() method is the issue here. Depending on the value of ReadAhead I will keeping getting the next chunk as soon as the last one is done (All) or not until a value is needed but not available (None) or only get the next chunk beyond the values that are currently being used (Some). Because of that, getting the next chunk should run in parallel/not block getting the next value. The enumerator code for this is:

private class ChunkedAsyncEnumerator<TState, TResult> : IAsyncEnumerator<TResult>
{
    private readonly ChunkedAsyncEnumerable<TState, TResult> enumerable;
    private readonly ConcurrentQueue<Task<TState>> chunks = new ConcurrentQueue<Task<TState>>();
    private readonly Queue<TResult> results = new Queue<TResult>();
    private CancellationTokenSource cts = new CancellationTokenSource();
    private TState lastState;
    private TResult current;
    private bool complete; // whether we have reached the end

    public ChunkedAsyncEnumerator(ChunkedAsyncEnumerable<TState, TResult> enumerable, TState initialState)
    {
        this.enumerable = enumerable;
        lastState = initialState;
        if(enumerable.ReadAhead != ReadAhead.None)
            chunks.Enqueue(NextChunk(initialState));
    }

    private async Task<TState> NextChunk(TState state, CancellationToken? cancellationToken = null)
    {
        await Task.Yield(); // ** causes deadlock
        var nextState = await enumerable.GetMore(state, cancellationToken ?? cts.Token).ConfigureAwait(false);
        if(enumerable.ReadAhead == ReadAhead.All && !enumerable.IsComplete(nextState))
            chunks.Enqueue(NextChunk(nextState)); // This is a read ahead, so it shouldn't be tied to our token

        return nextState;
    }

    public Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if(results.Count > 0)
        {
            current = results.Dequeue();
            return TaskConstants.True;
        }
        return complete ? TaskConstants.False : MoveNextAsync(cancellationToken);
    }

    private async Task<bool> MoveNextAsync(CancellationToken cancellationToken)
    {
        Task<TState> nextStateTask;
        if(chunks.TryDequeue(out nextStateTask))
            lastState = await nextStateTask.WithCancellation(cancellationToken).ConfigureAwait(false);
        else
            lastState = await NextChunk(lastState, cancellationToken).ConfigureAwait(false);

        complete = enumerable.IsComplete(lastState);
        foreach(var result in enumerable.GetResults(lastState))
            results.Enqueue(result);

        if(!complete && enumerable.ReadAhead == ReadAhead.Some)
            chunks.Enqueue(NextChunk(lastState)); // This is a read ahead, so it shouldn't be tied to our token

        return await MoveNext(cancellationToken).ConfigureAwait(false);
    }

    public TResult Current { get { return current; } }

    // Dispose() implementation omitted
}

I make no claim this code is perfect. Sorry it is so long, wasn't sure how to simplify. The important part is the NextChunk method and the call to Task.Yield(). This functionality is used through a static construction method:

internal static class AsyncEnumerableEx
{
    public static IAsyncEnumerable<TResult> GenerateChunked<TState, TResult>(
        TState initialState,
        Func<TState, CancellationToken, Task<TState>> getMore,
        Func<TState, IEnumerable<TResult>> getResults,
        Func<TState, bool> isComplete,
        ReadAhead readAhead = ReadAhead.None)
    { ... }
}
Community
  • 1
  • 1
Jeff Walker Code Ranger
  • 4,634
  • 1
  • 43
  • 62
  • 3
    That would be identical to `Task.Run()`, except more confusingly named. What would you expect it to do? – SLaks Feb 03 '15 at 21:36
  • 3
    Could you elaborate on why you needed to use `Task.Yield`? What were you trying to achieve? – Yuval Itzchakov Feb 03 '15 at 21:45
  • I use `Task.Yield()` in a method that is going to load more data asynchronously immediately before I call a callback that I can't know if it will run synchronously for a long time. I am trying to ensure that calling code isn't blocked by this operation which has nothing to do with the operation they are trying to perform, because I am preloading data ahead of what they requested right now. Think of something like loading the next page so it will be ready if the user requests it. – Jeff Walker Code Ranger Feb 03 '15 at 22:05
  • `ConfigureAwait(false)` is a nice performance optimization if you don't care about continuation context, but it shouldn't be used as a remedy against deadlocks. It can bring some surprises, check [this](http://stackoverflow.com/q/22672984/1768303) and [this](http://stackoverflow.com/a/28227165/1768303). – noseratio Feb 04 '15 at 01:36
  • 1
    @Jeff Why didn't you think to offload it via `Task.Run`? – Yuval Itzchakov Feb 04 '15 at 05:20
  • "before I call a callback that I can't know if it will run synchronously for a long time. I am trying to ensure that calling code isn't blocked by this operation which has nothing to do with the operation they are trying to perform" -- so you seem to be using `Yield()` not to actually yield, but just to "trick" the scheduler into shifting the work into a different thread. Why not just execute the callback asynchronously (i.e. using `Task.Run()`)? I agree with Yuval, the use of `Yield()` here seems very fishy. – Peter Duniho Feb 04 '15 at 06:49
  • @YuvalItzchakov I guess I didn't think to use `Task.Run` because I already had an async method with several awaits and thought "I want this method to return immediately so it doesn't block the caller." It was just the first step in my async operation. I wasn't thinking "I want to start up some new process" which is what `Task.Run` means in my mind. – Jeff Walker Code Ranger Feb 04 '15 at 12:18
  • @Noseratio I am aware of issues with continuation context. Though it is good to remember current culture is part of the context. This is a data access library that never needs the context. My personal opinion is that `SynchronizationContext` shouldn't exist and all async code should be written to not rely on context by wrapping any context dependencies and passing them into the task. However, I know that isn't always possible. – Jeff Walker Code Ranger Feb 04 '15 at 12:21
  • @PeterDuniho can you explain why the use of `Yield()` seems fishy? The [docs state](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.yield%28v=vs.110%29.aspx) "You can use await `Task.Yield();` in an asynchronous method to force the method to complete asynchronously." Which is exactly what I was using it for. Now of course they also go on to explain that context is preserved which was exactly what caused me the problem. – Jeff Walker Code Ranger Feb 04 '15 at 12:26
  • @PeterDuniho @Richard @WayneEllery @RobH Why is this question "too broad"? It seems there are really only two answers `Task.Run` and writing your own awaitable as @i3arnon did. Is it because of the second part "why is there no equivalent of `Task.Yield().ConfigureAwait(false)`"? – Jeff Walker Code Ranger Feb 04 '15 at 12:32
  • 1
    It's "too broad" because it's not _specific_. In particular, you haven't shown _any_ code, never mind enough to nail down why you are using `Yield()` so that anyone can offer a _good_ answer. And to be clear: I don't believe the answer you got is the one you need; writing a custom awaitable is a lot of extra trouble to go to when what's really wrong here is that `Yield()` was the wrong way to solve your immediate goal in the first place. The custom awaitable just adds a new hack on top of the original one. – Peter Duniho Feb 04 '15 at 17:25
  • 1
    And to be clear: that the answer solved the immediate problem, and even that you found it "good enough" to accept it, and even that some other people up-voted it, does not mean it's the answer you really needed. In some sense, it's good that _you_ feel your question was answered, but in the broader view SO is about generating answers that are beneficial to all, and this Q&A may well lead people in the wrong direction, if they don't take the time to read the dissenting comments. – Peter Duniho Feb 04 '15 at 17:27
  • @PeterDuniho This answer is the best answer for anybody searching for this question. It explains both the actual technical answer and what you should be doing in real code (both what the OP asked and what s/he needed). No one needs to read the comments to understand that. If you disagree you are more than welcome to provide a better answer. – i3arnon Feb 04 '15 at 21:58

3 Answers3

10

The exact equivalent of Task.Yield().ConfigureAwait(false) (which doesn't exist since ConfigureAwait is a method on Task and Task.Yield returns a custom awaitable) is simply using Task.Factory.StartNew with CancellationToken.None, TaskCreationOptions.PreferFairness and TaskScheduler.Current. In most cases however, Task.Run (which uses the default TaskScheduler) is close enough.

You can verify that by looking at the source for YieldAwaiter and see that it uses ThreadPool.QueueUserWorkItem/ThreadPool.UnsafeQueueUserWorkItem when TaskScheduler.Current is the default one (i.e. thread pool) and Task.Factory.StartNew when it isn't.

You can however create your own awaitable (as I did) that mimics YieldAwaitable but disregards the SynchronizationContext:

async Task Run(int input)
{
    await new NoContextYieldAwaitable();
    // executed on a ThreadPool thread
}

public struct NoContextYieldAwaitable
{
    public NoContextYieldAwaiter GetAwaiter() { return new NoContextYieldAwaiter(); }
    public struct NoContextYieldAwaiter : INotifyCompletion
    {
        public bool IsCompleted { get { return false; } }
        public void OnCompleted(Action continuation)
        {
            var scheduler = TaskScheduler.Current;
            if (scheduler == TaskScheduler.Default)
            {
                ThreadPool.QueueUserWorkItem(RunAction, continuation);
            }
            else
            {
                Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, scheduler);
            }
        }

        public void GetResult() { }
        private static void RunAction(object state) { ((Action)state)(); }
    }
}

Note: I don't recommend actually using NoContextYieldAwaitable, it's just an answer to your question. You should be using Task.Run (or Task.Factory.StartNew with a specific TaskScheduler)

Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • *it uses ThreadPool.QueueUserWorkItem/ThreadPool.UnsafeQueueUserWorkItem when TaskScheduler.Current is the default one and Task.Factory.StartNew when it isn't.* Don't forget that if there's a sync context it will simply post to it. I'm assuming that's what got him deadlocked. – Yuval Itzchakov Feb 03 '15 at 21:54
  • @YuvalItzchakov Of course, but OP is asking about the equivalence of `Task.Yield().ConfigureAwait(false)` (which doesn't exist) so the sync context is disregarded. – i3arnon Feb 03 '15 at 21:56
  • 4
    This whole `Task.Yield` business looks fishy :) Not sure how you end up taking that road – Yuval Itzchakov Feb 03 '15 at 21:58
  • What exactly would the "close enough" equivalent using Task.Run look like? Like this? `await Task.Run(() => {}).ConfigureAwait(false)` – Triynko Aug 25 '20 at 17:08
  • @Triynko You need to put the code that would have been after the `await Task.Yield` **inside** the `Task.Run` – i3arnon Aug 26 '20 at 18:09
10

I noticed you edited your question after you accepted the existing answer, so perhaps you're interested in more rants on the subject. Here you go :)

It's recommended that one use ConfigureAwait(false) whenever when you can, especially in libraries because it can help avoid deadlocks and improve performance.

It's recommended so, only if you're absolutely sure that any API you're calling in your implementation (including Framework APIs) doesn't depend on any properties of synchronization context. That's especially important for a library code, and even more so if the library is suitable for both client-side and server-side use. E.g, CurrentCulture is a common overlook: it would never be an issue for a desktop app, but it well may be for an ASP.NET app.

Back to your code:

private async Task<TState> NextChunk(...)
{
    await Task.Yield(); // ** causes deadlock
    var nextState = await enumerable.GetMore(...);
    // ...
    return nextState;
}

Most likely, the deadlock is caused by the client of your library, because they use Task.Result (or Task.Wait, Task.WaitAll, Task.IAsyncResult.AsyncWaitHandle etc, let them search) somewhere in the outer frame of the call chain. Albeit Task.Yield() is redundant here, this is not your problem in the first place, but rather theirs: they shouldn't be blocking on the asynchronous APIs and should be using "Async All the Way", as also explained in the Stephen Cleary's article you linked.

Removing Task.Yield() may or may not solve this problem, because enumerable.GetMore() can also use some await SomeApiAsync() without ConfigureAwait(false), thus posting the continuation back to the caller's synchronization context. Moreover, "SomeApiAsync" can happen to be a well established Framework API which is still vulnerable to a deadlock, like SendMailAsync, we'll get back to it later.

Overall, you should only be using Task.Yield() if for some reason you want to return to the caller immediately ("yield" the execution control back to the caller), and then continue asynchronously, at the mercy of the SynchronizationContext installed on the calling thread (or ThreadPool, if SynchronizationContext.Current == null). The continuation well may be executed on the same thread upon the next iteration of the app's core message loop. Some more details can be found here:

So, the right thing would be to avoid blocking code all the way. However, say, you still want to make your code deadlock-proof, you don't care about synchronization context and you're sure the same is true about any system or 3rd party API you use in your implementation.

Then, instead of reinventing ThreadPoolEx.SwitchTo (which was removed for a good reason), you could just use Task.Run, as suggested in the comments:

private Task<TState> NextChunk(...)
{
    // jump to a pool thread without SC to avoid deadlocks
    return Task.Run(async() => 
    {
        var nextState = await enumerable.GetMore(...);
        // ...
        return nextState;
    });
}

IMO, this is still a hack, with the same net effect, although a much more readable one than using a variation of ThreadPoolEx.SwitchTo(). Same as SwitchTo, it still has an associated cost: a redundant thread switch which may hurt ASP.NET performance.

There is another (IMO better) hack, which I proposed here to address the deadlock with aforementioned SendMailAsync. It doesn't incur an extra thread switch:

private Task<TState> NextChunk(...)
{
    return TaskExt.WithNoContext(async() => 
    {
        var nextState = await enumerable.GetMore(...);
        // ...
        return nextState;
    });
}

public static class TaskExt
{
    public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func)
    {
        Task<TResult> task;
        var sc = SynchronizationContext.Current;
        try
        {
            SynchronizationContext.SetSynchronizationContext(null);
            task = func(); // do not await here
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(sc);
        }
        return task;
    }
}

This hack works in the way it temporarily removes the synchronization context for the synchronous scope of the original NextChunk method, so it won't be captured for the 1st await continuation inside the async lambda, effectively solving the deadlock problem.

Stephen has provided a slightly different implementation while answering the same question. His IgnoreSynchronizationContext restores the original synchronization context on whatever happens to be the continuation's thread after await (which could be a completely different, random pool thread). I'd rather not restore it after await at all, as long as I don't care about it.

Timo
  • 7,992
  • 4
  • 49
  • 67
noseratio
  • 59,932
  • 34
  • 208
  • 486
2

Inasmuch as the useful and legit API you're looking for is missing, I filed this request proposing its addition to .NET.

I also added it to vs-threading so that the next release of the Microsoft.VisualStudio.Threading NuGet package will include this API. Note that this library is not VS-specific, so you can use it in your app.

Andrew Arnott
  • 80,040
  • 26
  • 132
  • 171