3

Consider the following code:

async Task Go()
{
   var task1 = PrintAnswerToLife();
   var task2 = PrintAnswerToLife();
   await task1; await task2;
}

async Task PrintAnswerToLife()
{
   var task = GetAnswerToLife();
   int answer = await task; Console.WriteLine(answer);
}

async Task<int> GetAnswerToLife()
{
   var task = Task.Delay(5000);
   await task; int answer = 21 * 2; return answer
}

Question 1:

In chapter 14, page 588, of the book "C# 5.0 in a Nutshell" by the Albahari brothers, it is stated that the two asynchronous operations task1 and task2 run in parallel. That does not seem correct to me. As far as I understand, when var task1 = PrintAnswerToLife(); runs, the executions goes into PrintAnswerToLife() where when it hits await returns execution to Go(), and proceeds to the next line var task1 = PrintAnswerToLife();, where the same thing happens again. In other words there is nothing occurring in parallel in the first two lines of Go(). In fact, unless there is a thread involved (as in Task.Run() or Task.Delay()), no real parallelism ever occurs. Have I understood this correctly? If so, what does Albahari really mean by saying that the two operations run in parallel?

Question 2:

On the same page, Albahari goes on to state the following:

Concurrency created in this manner occurs whether or not the operations are initiated on a UI thread, although there is a difference in how it occurs. In both cases, we get the same concurrency occurring in the bottom-level operations that initiate it (such as Task.Delay, or code farmed to Task.Run). Methods above this in the call stack will be subject to true concurrency only if the operation was initiated without a synchronization context present...

What does Albahari mean by this? I do not understand how a SynchronizationContext comes into play here, and what difference it makes.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Anders
  • 580
  • 8
  • 17
  • You seem to assume that `Task.Delay` uses threads. It doesn't. It returns a Task that magically completes after some time. – usr Oct 12 '14 at 18:20
  • @usr There is no magic involved hehe. Task.Delay uses System.Timers.Timer, which fires the Elapsed event on the system thread pool. See: http://msdn.microsoft.com/en-us/library/system.timers.timer(v=vs.110).aspx – Anders Oct 12 '14 at 18:41
  • @Anders *`System.Threading.Timer`... – i3arnon Oct 12 '14 at 20:19

2 Answers2

4

In fact, unless there is a thread involved (as in Task.Run() or Task.Delay()), no real parallelism ever occurs. Have I understood this correctly?

A thread has to be involved only if we're talking about parallelism. Your example is about concurrency

Lets break it down:

  1. You execute PrintAnswerToLife, which in turn runs GetAnswerToLife, and right there it hits its first await on Task.Delay(5000). Once hit, return will yield control back to PrintAnswerToLife, which will then itself await the Task<int> returned, which will cause execution to yield back to Go. Meanwhile, start the execution of your second call to PrintAnswerToLife

  2. This same cycle is executed for task2.

  3. You then sequentially await each Task. You could easily await on them concurrently using Task.WhenAll.

What does Albahari mean by this? I do not understand how a SynchronizationContext comes into play here, and what difference it make?

A SynchronizationContext is in charge of your execution flow. In .NET we have various SynchronizationContext's, such as the DispatcherSynchronizationContext and the WinFormSynchronizationContext, which are responsible for marshaling work back onto the UI thread (WPF and WinForms, respectively). I think what he's trying to point out is the fact that each of those SynchronizationContext will eventually marshal control back to some sort of UI message loop, which will be forced to execute synchronously, one after the other. If there isn't a SynchronizationContext, the default used is the ThreadPoolSynchronizationContext, which will invoke the continuation of those tasks on an arbitary threadpool thread. This isn't entirely true, since one can avoid the marshaling of the context back to the UI thread using ConfigureAwait(false).

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 1
    Yuval, thanks for the info. On Question 1 you say that I have not understood it correctly, but your explanation indicates that I have. As per both yours and my explanation, there is no parallelism occurring until Task.Delay runs (it uses a Timer, which in turn, when Elapsed, executes SetResult of a TaskCompletionSource on a separate thread in the thread pool). So, nothing is run in parallel here, without the Task.Delay. Do you see what I mean? – Anders Oct 12 '14 at 17:12
  • I understand. But even though `Task.Delay` exists in your code, it isn't executing in parallel, its concurrent, there is a difference. I have corrected my answer to state what i ment. – Yuval Itzchakov Oct 12 '14 at 17:14
  • 1
    @YuvalItzchakov, @i3arnon The expressions after the `await` in `PrintAnswerToLife` and `GetAnswerToLife` are translated into continuations by the compiler. These continuations are executed on ThreadPool threads, so there is a potential for parallelism. – participant Oct 12 '14 at 19:31
  • 1
    @participant If a synchronization context exists, These continuations will be marshaled back to the UI thread. Only if OP uses `ConfigureAwait(false)` the execution flow will continue on the IOCP. – Yuval Itzchakov Oct 13 '14 at 09:07
2

Question 1:

Parallel usually means multiple threads processing simultaneously. The more accurate term to use here is concurrent. In your example both tasks are executed concurrently. That means that the asynchronous operation (i.e. await Task.Delay(5000)) in both tasks is "executed" at the same time. That's why both tasks would complete about 5 seconds since Go began. If the tasks would have been run sequentially it would have taken 10 seconds. The synchronous continuations would then be scheduled on a thread pool thread (assuming there's no special SynchronizationContext) and there there's a chance for parallelism.

Have I understood this correctly?

Yes.

If so, what does Albahari really mean by saying that the two operations run in parallel?

That they run concurrently and asynchronously with potentially parallel continuations.

Question 2:

Again the explanation is a bit simplistic. It refers to specific SynchronizationContexts, the single threaded SynchronizationContexts used in GUI environments. Because this SynchronizationContext schedules all work on a single specific thread it doesn't allow for "true concurrency". However there are other, multithreaded SynchronizationContexts and you can create your own. Therefore using a SynchronizationContext doesn't necessarily hinder concurrency (and you can also disable the SynchronizationContext capture altogether by using ConfigureAwait(false) on a task)

What does Albahari mean by this?

In GUI environments the SynchronizationContext uses a single thread that can't execute anything in parallel.

Community
  • 1
  • 1
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • *In other words there is nothing occurring in parallel in the first two lines of Go(). In fact, unless there is a thread involved (as in Task.Run() or Task.Delay()), no real parallelism ever occurs.* This is what he means, are you sure he understood it correctly? – Yuval Itzchakov Oct 12 '14 at 17:09
  • @YuvalItzchakov If by parallel we mean by multiple threads, then yes. The asynchronous part is run concurrently but if there was a synchronization context there this whole method could have been executed with a single thread. – i3arnon Oct 12 '14 at 17:12
  • I think he really ment concurrent, not parallel. – Yuval Itzchakov Oct 12 '14 at 17:13
  • 1
    @YuvalItzchakov I agree. *"Parallel usually means multiple threads processing simultaneously. The more accurate term to use here is concurrent."* – i3arnon Oct 12 '14 at 17:14
  • l3arnon, thanks for the answer. Task.Delay is not executed at exactly the same time though, right? It does in fact not happen concurrently. What happens is that Task.Delay is exacuted once for task1, then again for task2 (shortly after, but not at the same time). There is no parallelism or concurrency here until Elapsed fires in Task.Delay. The reason why you get both answers printed after about 5 seconds, is because Task.Delay in fact starts a new thread on the thread pool (see my comment to the other post for more detail). Without threads there is no real concurrency. Right? – Anders Oct 12 '14 at 17:19
  • @Anders no. The concurrency here is the actual "execution" of the asynchronous operation (in your case the entire 5 seconds both tasks wait for). There are no threads involved though. In real cases that means the you can have multiple async IO operations running concurrently while you are using no threads until they complete. – i3arnon Oct 12 '14 at 17:22
  • 1
    I was actually misusing the word Concurrency. This sorted my confusion: http://stackoverflow.com/questions/1050222/concurrency-vs-parallelism-what-is-the-difference. Task.Delay actually uses the system thread pool implicitly: http://msdn.microsoft.com/en-us/library/system.timers.timer(v=vs.110).aspx. There is no parallelism before the Timer elapses, agreed? – Anders Oct 12 '14 at 18:44
  • @Anders agreed, there's not. And if a single threaded synchronization context is used, then there's also no parallelism after it. – i3arnon Oct 12 '14 at 18:50