8

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.

Water Cooler v2
  • 32,724
  • 54
  • 166
  • 336

2 Answers2

16

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • As I understand, providing `TaskScheduler.Default` is the same as supplying flag `TaskContinuationOptions.HideScheduler`. – Mike Jan 03 '21 at 11:55
  • 1
    @Mike: No; `TaskScheduler.Default` runs the delegate on a thread pool thread. `TaskContinuationOptions.HideScheduler` will ensure `TaskScheduler.Current` is `TaskScheduler.Default`, even if the code is *not* running on the default task scheduler. – Stephen Cleary Jan 04 '21 at 03:32
  • @StephenCleary what that mean? make sure current is default ? you recently said default is thread pool, and current is current whatever it is to that execution... so it can never be the other thing ... :/ – Hassan Faghihi Dec 20 '21 at 06:26
  • 1
    @deadManN: The terminology here is confusing. The bottom line is that you should *always* pass a `TaskScheduler` to `ContinueWith` (and `StartNew`). The `TaskScheduler.Default` property is the thread pool, but the default value of the `TaskScheduler` instance used by `ContinueWith` and `StartNew` is not `TaskScheduler.Default`. The central takeaway is to always pass a `TaskScheduler`. Or just use `await` instead of `ContinueWith` and `Run` instead of `StartNew`, which will make your code much more maintainable. – Stephen Cleary Dec 21 '21 at 02:35
4

I've looked into the reference source of Task then ConfiguredTaskAwaitable via GetAwaiter.OnCompleted back through into Task.SetContinuationForAwait (with continueOnCapturedContext as false), and it drops into:

if (!AddTaskContinuation(tc, addBeforeOthers: false))
        tc.Run(this, bCanInlineContinuationTask: false);

which is, basically, ContinueWith. So: yes.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900