Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ConfigureAwait(false) when using ContinueWith

Tags:

c#

What's the equivalent of using:

await task.ConfigureAwait(false);

when using continuations like so (without using the C# compiler's async/await transformations)?

var taskOfString = ScheduleWorkOnThreadPoolAsync();

// I'd like this continuation to not have to
// "flow" the synchronization context but to simply
// execute wherever it can, i.e. I'd like to tell is
// ConfigureAwait(false) for its previous task.
// How do I do that?
taskOfString.ContinueWith(t => { });


public async Task<string> ScheduleWorkOnThreadPoolAsync()
{
  return Task.Run(() => return "Foo" );
}

I am assuming that doing nothing, i.e. just leaving it as is is equivalent to calling ConfigureAwait(false), which is also what I see happening when I debug the code. It hops on whatever thread it can.

It is only when we want to specify a scheduler or synchronization context to run the continuation on that we need to pass in extra information to the overload that accepts a TaskScheduler. Otherwise, it is defaulted to run without any regard to the execution context.

However, I still request a confirmation or correction if I am wrong.

like image 250
Water Cooler v2 Avatar asked May 23 '17 08:05

Water Cooler v2


1 Answers

when using continuations like so (without using the C# compiler's async/await transformations)?

You should almost always use async/await. They have far safer default behavior. ContinueWith is a dangerous, low-level API.

I am assuming that doing nothing, i.e. just leaving it as is is equivalent to calling ConfigureAwait(false)... It is only when we want to specify a scheduler or synchronization context to run the continuation on that we need to pass in extra information to the overload that accepts a TaskScheduler. Otherwise, it is defaulted to run without any regard to the execution context.

No. This is incorrect, although simple tests won't reveal the problem.

As I describe in my blog post on why you shouldn't use ContinueWith, the default TaskScheduler for ContinueWith is not TaskScheduler.Default. It's TaskScheduler.Current. Since this is confusing in every scenario, you should always pass a TaskScheduler to ContinueWith and StartNew.

If you want await x.ConfigureAwait(false) behavior, you would actually do:

var continuation = x.ContinueWith(callback, CancellationToken.None,
    TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.DenyChildAttach,
    TaskScheduler.Default);

The TaskContinuationOptions.ExecuteSynchronously emulates the synchronous-if-possible behavior of await. The TaskContinuationOptions.DenyChildAttach prevents problems if the continuation task is attached to (tasks intended for asynchronous have surprising behavior when child tasks attach to them with AttachedToParent). The TaskScheduler.Default emulates the ConfigureAwait(false) behavior of always executing on a thread pool context.

As a final note, you'll probably need to do something with continuation - at least observe it for exceptions and handle them somehow.

At this point, it should be clear why I recommend await. At worst, you'd just need to add a helper method to use await instead of ContinueWith - await is much more maintainable, IMO.

like image 163
Stephen Cleary Avatar answered Oct 06 '22 14:10

Stephen Cleary