When should Task.ContinueWith be called with TaskScheduler.Current as an argument?

continuewith vs await
task continuewith taskscheduler
c# taskscheduler current
task run context
task factory startnew ui thread
task factory startnew priority
task factory startnew synchronous
task factory startnew longrunning

We are using this code snippet from StackOverflow to produce a Task that completes as soon as the first of a collection of tasks completes successfully. Due to the non-linear nature of its execution, async/await is not really viable, and so this code uses ContinueWith() instead. It doesn't specify a TaskScheduler, though, which a number of sources have mentioned can be dangerous because it uses TaskScheduler.Current when most developers usually expect TaskScheduler.Default behavior from continuations.

The prevailing wisdom appears to be that you should always pass an explicit TaskScheduler into ContinueWith. However, I haven't seen a clear explanation of when different TaskSchedulers would be most appropriate.

What is a specific example of a case where it would be best to pass TaskScheduler.Current into ContinueWith(), as opposed to TaskScheduler.Default? Are there rules of thumb to follow when making this decision?

For context, here's the code snippet I'm referring to:

public static Task<T> FirstSuccessfulTask<T>(IEnumerable<Task<T>> tasks)
{
    var taskList = tasks.ToList();
    var tcs = new TaskCompletionSource<T>();
    int remainingTasks = taskList.Count;
    foreach(var task in taskList)
    {
        task.ContinueWith(t =>
            if(task.Status == TaskStatus.RanToCompletion)
                tcs.TrySetResult(t.Result));
            else
                if(Interlocked.Decrement(ref remainingTasks) == 0)
                    tcs.SetException(new AggregateException(
                        tasks.SelectMany(t => t.Exception.InnerExceptions));
    }
    return tcs.Task;
}

Task.ContinueWith Method (System.Threading.Tasks), ContinueWith<TResult>(Func<Task,Object,TResult>, Object, TaskScheduler) The returned Task will not be scheduled for execution until the current task has When run, the delegate will be passed the completed task as an argument. by the time ContinueWith is called, the synchronous continuation will run on the  It doesn't specify a TaskScheduler, though, which a number of sources have mentioned can be dangerous because it uses TaskScheduler.Current when most developers usually expect TaskScheduler.Default behavior from continuations. The prevailing wisdom appears to be that you should always pass an explicit TaskScheduler into ContinueWith.

I'll have to rant a bit, this is getting way too many programmers into trouble. Every programming aid that was designed to make threading look easy creates five new problems that programmers have no chance to debug.

BackgroundWorker was the first one, a modest and sensible attempt to hide the complications. But nobody realizes that the worker runs on the threadpool so should never occupy itself with I/O. Everybody gets that wrong, not many ever notice. And forgetting to check e.Error in the RunWorkerCompleted event, hiding exceptions in threaded code is a universal problem with the wrappers.

The async/await pattern is the latest, it makes it really look easy. But it composes extraordinarily poorly, async turtles all the way down until you get to Main(). They had to fix that eventually in C# version 7.2 because everybody got stuck on it. But not fixing the drastic ConfigureAwait() problem in a library. It is completely biased towards library authors knowing what they are doing, notable is that a lot of them work for Microsoft and tinker with WinRT.

The Task class bridged the gap between the two, its design goal was to make it very composable. Good plan, they could not predict how programmers were going to use it. But also a liability, inspiring programmers to ContinueWith() up a storm to glue tasks together. Even when it doesn't make sense to do so because those tasks merely run sequentially. Notable is that they even added an optimization to ensure that the continuation runs on the same thread to avoid the context switch overhead. Good plan, but creating the undebuggable problem that this web site is named for.

So yes, the advice you saw was a good one. Task is useful to deal with asynchronicity. A common problem that you have to deal with when services move into the "cloud" and latency gets to be a detail you can no longer ignore. If you ContinueWith() that kind code then you invariably care about the specific thread that executes the continuation. Provided by TaskScheduler, low odds that it isn't the one provided by FromCurrentSynchronizationContext(). Which is how async/await happened.

A Tour of Task, Part 7: Continuations, The task that a continuation attaches to is called the “antecedent” task. Task ContinueWith(Action<Task>, TaskScheduler); Task The next parameter is TaskContinuationOptions , a collection of options for the continuation. but then will pretend that there is no current task scheduler while the  An action to run when the Task completes. When run, the delegate will be passed the completed task and the caller-supplied state object as arguments. An object representing data to be used by the continuation action. cancellationToken. CancellationToken. The CancellationToken that will be assigned to the new continuation task. continuationOptions.

If current task is a child task, then using TaskScheduler.Current will mean the scheduler will be that which the task it is in, is scheduled to; and if not inside another task, TaskScheduler.Current will be TaskScheduler.Default and thus use the ThreadPool.

If you use TaskScheduler.Default, then it will always go to the ThreadPool.

The only reason you would use TaskScheduler.Current:

To avoid the default scheduler issue, you should always pass an explicit TaskScheduler to Task.ContinueWith and Task.Factory.StartNew.

From Stephen Cleary's post ContinueWith is Dangerous, Too.

There's further explanation here from Stephen Toub on his MSDN blog.

C# Precisely, A Task represents an asynchronous activity that does not produce a result Task ContinueWith(Action<Task> cont) creates and starts a task in which the given task the computation cont; that is, when task completes, cont(task) will be called. starts the given task for synchronous execution on the current task scheduler;  There are a few more “scheduling options” that are not passed to the TaskScheduler.The HideScheduler option (introduced in .NET 4.5) will use the given task scheulder to schedule the continuation, but then will pretend that there is no current task scheduler while the continuation is executing; this can be used as a workaround for the unexpected default task scheduler (described below).

referencesource/Task.cs at master · microsoft/referencesource · GitHub, Represents the current stage in the lifecycle of a <see cref="Task"/>. internal TaskScheduler m_taskScheduler; // The task scheduler this task runs under. (​i.e. whether the last child or this task itself should call FinishStageTwo()) The <​paramref name="creationOptions"/> argument specifies an invalid value for <see. Çağıranın sağladığı durum bilgilerini ve bir iptal belirtecini alan ve hedef Task tamamlandığında yürüten bir devamlılık oluşturur. Creates a continuation that receives caller-supplied state information and a cancellation token and that executes when the target Task completes.

Why is TaskScheduler.Current the default TaskScheduler?, Current will reuse the synchronization context task scheduler from my library continuation. Finding out this could take some time, especially if the second task call is buried This has the disadvantage that you have to specify type argument if the task However, Task.ContinueWith has a hardcoded TaskScheduler.​Current. Unfortunately, I see developers making the same mistake with Task.ContinueWith. One of the main problems of StartNew is that it has a confusing default scheduler. This exact same problem also exists in the ContinueWith API. Just like StartNew, ContinueWith will default to TaskScheduler.Current, not TaskScheduler.Default.

Dixin's Blog, A task can be waited by calling its Wait method, which blocks the current thread until the When Write is called, its execution blocks the current thread. There is no ContinueWith call needed to build the continuation. The parameter of the workflow is compiled as a field of the state machine, so it can be  ContinueWith(Action<Task<TResult>,Object>, Object, CancellationToken, TaskContinuationOptions, TaskScheduler) Creates a continuation that executes when the target Task<TResult> completes.

C# Task, If the task is not finished, this call will block the current thread. The ContinueWith method has a couple of overloads that you can use to of running into a separate thread by using the task scheduler parameter in task. In asynchronous programming, it is common for one asynchronous operation, on completion, to invoke a second operation and pass data to it. Traditionally, continuations have been done by using callback methods. In the Task Parallel Library, the same functionality is provided by continuation tasks. A continuation task (also known just as a

Comments
  • Thank you. You've given specific, code-based examples of when various schedulers would be appropriate, and specifically addressed the underlying question of what task scheduler to use in the code I was considering. This is the kind of answer I was looking for.
  • Much appreciated rant. Maybe you can clarify a bit some of the wording. In particular the last sentence is a bit terse and I'm not at all sure what it meant to say.
  • Not sure how to improve it, the continuation after await is biased to run on the UI thread to make it easy to update the user interface. Forcing programmers to monkey with ConfigureAwait() in a library and not actually knowing what to pass because they don't know how their library is getting used.
  • The rant is somewhat informational, but I still don't feel like it answered the question. Are you saying that because my method doesn't know how it's going to be used, there's no way for me to know which TaskScheduler I should use for these continuations?
  • I kind of feel like this answer mostly reiterated the background I already provided for my question, but didn't answer the question part. Can you speak to specific examples or rules of thumb about when to use TaskScheduler.Current?
  • Thank you for answering the question. This gives me something to go on.