How to Prevent Synchronous Continuations on a Task

How can I prevent synchronous continuations on a Task?

New in .NET 4.6:

.NET 4.6 contains a new TaskCreationOptions: RunContinuationsAsynchronously.


Since you're willing to use Reflection to access private fields...

You can mark the TCS's Task with the TASK_STATE_THREAD_WAS_ABORTED flag, which would cause all continuations not to be inlined.

const int TASK_STATE_THREAD_WAS_ABORTED = 134217728;

var stateField = typeof(Task).GetField("m_stateFlags", BindingFlags.NonPublic | BindingFlags.Instance);
stateField.SetValue(task, (int) stateField.GetValue(task) | TASK_STATE_THREAD_WAS_ABORTED);

Edit:

Instead of using Reflection emit, I suggest you use expressions. This is much more readable and has the advantage of being PCL-compatible:

var taskParameter = Expression.Parameter(typeof (Task));
const string stateFlagsFieldName = "m_stateFlags";
var setter =
Expression.Lambda<Action<Task>>(
Expression.Assign(Expression.Field(taskParameter, stateFlagsFieldName),
Expression.Or(Expression.Field(taskParameter, stateFlagsFieldName),
Expression.Constant(TASK_STATE_THREAD_WAS_ABORTED))), taskParameter).Compile();

Without using Reflection:

If anyone's interested, I've figured out a way to do this without Reflection, but it is a bit "dirty" as well, and of course carries a non-negligible perf penalty:

try
{
Thread.CurrentThread.Abort();
}
catch (ThreadAbortException)
{
source.TrySetResult(123);
Thread.ResetAbort();
}

Configuring the continuation behaviour of a TaskCompletionSource's Task

There is no way to prevent synchronous task continuations. Normally, this is not a problem.

However, there are some situations where you do need this, e.g., if you are completing the task while holding a lock. In those cases, you can just Task.Run the task completion, as such:

// Set the result on a threadpool thread, so any synchronous continuations
// will execute in the background.
Task.Run(() => tcs.TrySetResult(result));

// Wait for the TCS task to complete; note that the continuations
// may not be complete.
tcs.Task.Wait();

This is an advanced technique. It is an exception to the guideline "Don't block on async code (async all the way down)" as expounded on my blog.

It's part of my AsyncEx library, as an extension method:

public static void TrySetResultWithBackgroundContinuations<TResult>(
this TaskCompletionSource<TResult> @this, TResult result);

This technique was first published by Stephen Toub on his blog.

Prevent a Task Continuation

From MSDN

a user-defined value can be passed from the antecedent to its
continuation in the Result property, so that the output of the
antecedent can serve as input for the continuation

So one way would be in task3, check the Result from task2 before doing any work.

var task2 = task1.ContinueWith(result => DoSomeMoreWOrk(), TaskContinuationOptions.OnlyOnRanToCompletion);
var task3 = task2.ContinueWith(x => DoFinalWork(x.Result));

Where the Result returned from task2 determines what happens in task3.

EDIT
Another solution would be to cancel task2 if certain conditions are not met within the operation. Then the continuation task is not even scheduled.

From MSDN

To prevent a continuation from executing if its antecedent is
canceled, specify the NotOnCanceled option when you create the
continuation.

So task3 definition becomes

var task3 = task2.ContinueWith(result => DoFinalWork(), TaskContinuationOptions.NotOnCancelled);

C# How to prevent Task from conflicting with running Task?

One option would be to use a limited concurrency task scheduler (see example). This can ensure that each task is run sequentially.

Another option is to start a thread that waits on a autoResetEvent in a loop. The timer would simply trigger the autoResetEvent, releasing the thread to do its job. The downside is that this would consume a whole thread.

A third option would be to set a flag when starting the work, and reset it when it is complete. The timer could then skip starting the task if the flag is set. You might want to use lock or interlocked to avoid thread safety issues.

There might be some issues when adding items just before the task completes, but if it is run on a timer I would assume all items would be processed eventually.

TaskContinuationOptions.RunContinuationsAsynchronously and Stack Dives

The key concept here is that a task's continuation may run synchronously on the same thread that completed the antecedent task.

Let's imagine that this is SemaphoreSlim.Release's implementation (it's actually Toub's AsyncSemphore's):

public void Release() 
{
TaskCompletionSource<bool> toRelease = null;
lock (m_waiters)
{
if (m_waiters.Count > 0)
toRelease = m_waiters.Dequeue();
else
++m_currentCount;
}
if (toRelease != null)
toRelease.SetResult(true);
}

We can see that it synchronously completes a task (using TaskCompletionSource).
In this case, if WorkAsync has no other asynchronous points (i.e. no awaits at all, or all awaits are on an already completed task) and calling _gate.Release() may complete a pending call to _gate.WaitAsync() synchronously on the same thread you may reach a state in which a single thread sequentially releases the semaphore, completes the next pending call, executes // work here and then releases the semaphore again etc. etc.

This means that the same thread goes deeper and deeper in the stack, hence stack dive.

RunContinuationsAsynchronously makes sure the continuation doesn't run synchronously and so the thread that releases the semaphore moves on and the continuation is scheduled for another thread (which one depends on the other continuation parameters e.g. TaskScheduler)

This logically resembles posting the completion to the ThreadPool:

public void Release() 
{
TaskCompletionSource<bool> toRelease = null;
lock (m_waiters)
{
if (m_waiters.Count > 0)
toRelease = m_waiters.Dequeue();
else
++m_currentCount;
}
if (toRelease != null)
Task.Run(() => toRelease.SetResult(true));
}

TaskContinuations ran synchronously with async lambdas

I second Marc's comment: you should avoid ContinueWith; it's a low-level API that has potentially dangerous default behavior.

The part after the await - will it effectively release the thread that started executing the task continuation?

The await will do that, yes.

And on which thread will the rest of this continuation resume? ... The question is asked when there is no SynchronizationContext present for the antecedent task of the continuation above

Most likely, it would be the thread pool thread that completes the Task returned by Task.Delay. This is because await uses TaskContinuationOptions.ExecuteSynchronously (note: this is an implementation detail; do not depend on this behavior).

I say "most likely" because there are situations where ExecuteSynchronously doesn't execute synchronously. It's an optimization hint, not a requirement. This also applies to the part of your continuation before the await: it is most likely (not definitely) running on the thread that completed the antecedent task.

but I've heard that SynchronziationContext does not flow through ContinueWith method calls? Is that true?

ContinueWith works at the task level. It has some logic for flowing through TaskScheduler.Current (which IMO is one aspect that makes this API dangerous), but has no awareness of SynchronizationContext.

How can I guarantee continuations run in task completion order?

If you use TAP (Task Asynchronous Programming), i.e. async and await, you can make the flow of processing a lot more apparent. In this case I would create a new method to encapsulate the order of operations:

public async Task ProcessStagedOperation(StagedOperation op, int i)
{
await op.Connect;
Console.WriteLine("{0}: Connected", i);

await op.Accept;
Console.WriteLine("{0}: Accepted", i)

await op.Complete;
Console.WriteLine("{0}: Completed", i)
}

Now your processing loop gets simplified a bit:

public async Task RunOperations()
{
List<Task> pendingOperations = new List<Task>();

for (int i=0; i<3; i++)
{
var op = InitiateStagedOperation(i);
pendingOperations.Add(ProcessStagedOperation(op, i));
}

await Task.WhenAll(pendingOperations); // finish
}

You now have a reference to a task object you can explicitly wait or simply await from another context. (or you can simply ignore it). The way I modified the RunOperations() method allows you to create a large queue of pending tasks but not block while you wait for them all to finish.

Can I guarantee runnable task continuations have been run?

Can I guarantee runnable task continuations have been run?

By awaiting them.

var i = 0;

var taskCompletionSource = new TaskCompletionSource<object>();
var task = taskCompletionSource.Task;

var continuation = Task.Run(async () =>
{
await task;
i = 1;
});

// Synchronously complete `task`
taskCompletionSource.SetResult(null);

// wait for the continuation
await continuation;

// ouputs 1
Console.WriteLine(i);

That works if you're withing an asynchronous method. If you're not, make it asynchronous. You can technically block, too, (.Wait() instead of await) but that invites deadlocks, so be careful.



Related Topics



Leave a reply



Submit