Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TaskContinuationOptions.RunContinuationsAsynchronously and Stack Dives

Tags:

In this blog post, Stephan Toub describes a new feature that will be included in .NET 4.6 which adds another value to the TaskCreationOptions and TaskContinuationOptions enums called RunContinuationsAsynchronously.

He explains:

"I talked about a ramification of calling {Try}Set* methods on TaskCompletionSource, that any synchronous continuations off of the TaskCompletionSource’s Task could run synchronously as part of the call. If we were to invoke SetResult here while holding the lock, then synchronous continuations off of that Task would be run while holding the lock, and that could lead to very real problems. So, while holding the lock we grab the TaskCompletionSource to be completed, but we don’t complete it yet, delaying doing so until the lock has been released"

And gives the following example to demonstrate:

private SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private async Task WorkAsync() {     await _gate.WaitAsync().ConfigureAwait(false);     try     {         // work here     }     finally { _gate.Release(); } } 

Now imagine that you have lots of calls to WorkAsync:

await Task.WhenAll(from i in Enumerable.Range(0, 10000) select WorkAsync()); 

We've just created 10,000 calls to WorkAsync that will be appropriately serialized on the semaphore. One of the tasks will enter the critical region, and the others will queue up on the WaitAsync call, inside SemaphoreSlim effectively enqueueing the task to be completed when someone calls Release. If Release completed that Task synchronously, then when the first task calls Release, it'll synchronously start executing the second task, and when it calls Release, it'll synchronously start executing the third task, and so on. If the "//work here" section of code above didn't include any awaits that yielded, then we're potentially going to stack dive here and eventually potentially blow out the stack.

I'm having a hard time grasping the part where he talks about executing the continuation synchronously.

Question

How could this possibly cause a stack dive? More so, And what is RunContinuationsAsynchronously effectively going to do in order to solve that problem?

like image 271
Yuval Itzchakov Avatar asked Feb 04 '15 12:02

Yuval Itzchakov


1 Answers

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));  } 
like image 149
i3arnon Avatar answered Oct 13 '22 03:10

i3arnon