5

According to MSDN, async and await do not create new threads:

The async and await keywords don't cause additional threads to be created.

With this in mind, I'm having difficulty understanding control flow of some simple programs. My complete example is below. Note that it requires the Dataflow library, which you can install from NuGet.

using System;
using System.Threading.Tasks.Dataflow;

namespace TaskSandbox
{
    class Program
    {
        static void Main(string[] args)
        {
            BufferBlock<int> bufferBlock = new BufferBlock<int>();

            Consume(bufferBlock);
            Produce(bufferBlock);

            Console.ReadLine();
        }

        static bool touched;
        static void Produce(ITargetBlock<int> target)
        {
            for (int i = 0; i < 5; i++)
            {
                Console.Error.WriteLine("Producing " + i);
                target.Post(i);
                Console.Error.WriteLine("Performing intensive computation");
                touched = false;
                for (int j = 0; j < 100000000; j++)
                    ;
                Console.Error.WriteLine("Finished intensive computation. Touched: " + touched);
            }

            target.Complete();
        }

        static async void Consume(ISourceBlock<int> source)
        {
            while (await source.OutputAvailableAsync())
            {
                touched = true;
                int received = source.Receive();
                Console.Error.WriteLine("Received " + received);
            }
        }
    }
}

Output:

Producing 0
Performing intensive computation
Received 0
Finished intensive computation. Touched: True
Producing 1
Performing intensive computation
Received 1
Finished intensive computation. Touched: True
Producing 2
Performing intensive computation
Received 2
Finished intensive computation. Touched: False
Producing 3
Performing intensive computation
Received 3
Finished intensive computation. Touched: False
Producing 4
Performing intensive computation
Received 4
Finished intensive computation. Touched: True

This seems to indicate that Consume is given control while the for loop is running, as the OutputAvailableAsync task completes:

for (int j = 0; j < 100000000; j++)
    ;

This would be unsurprising in a threaded model. But if no additional threads are involved, how can Produce yield control in the middle of the for loop?

Matthew
  • 28,056
  • 26
  • 104
  • 170
  • @I4V: The answer to that question states that "it's necessary that all blocking operations explicitly yield control using the async/await model." But in my question, control passes from `Produce` to `Consume` without `Produce` explicitly yielding control. That is the part that I am confused about. – Matthew May 21 '13 at 20:58
  • 3
    @Matthew Since this is a Console application there is no `SynchronizationContext`, which means that all of the callbacks from `await` calls go to `SynchronizationContext.Default`, which is the thread pool, so technically there are actually two threads running at times during the execution of this program. To prevent the creation of additional threads you'd need to create your own custom sync context and set that. If you did, you'd see that the `Recieved` calls wouldn't be called until all of the producing was done specifically because you're *not* yielding control while producing. – Servy May 21 '13 at 21:01
  • @Servy: If there are multiple threads involved, why does MSDN claim that "In particular, this approach is better than BackgroundWorker for IO-bound operations because...you don't have to guard against race conditions"? I edited my example to add a simple race condition. – Matthew May 21 '13 at 21:16
  • 2
    @Matthew Using `await` doesn't *necessarily* involve the creation of thread. You can use it in ways that don't ever create threads. You simply haven't done so. Also note that it's using the thread pool thread just to run the callback. That thread pool thread isn't *blocking* when it has nothing to do, it's actually doing nothing and is released back to the pool; it's capable of handling another request. This is important because it means you don't have 100 thread pool thread sitting there blocking, waiting for IO to be completed. – Servy May 21 '13 at 21:19
  • 2
    @Matthew You may find [my `async`/`await` intro](http://blog.stephencleary.com/2012/02/async-and-await.html) helpful. I tried to cover the basics (with all relevant details) in a single post. – Stephen Cleary May 21 '13 at 21:39
  • @Servy: If you post your comment as an answer (regarding `SynchronizationContext.Default`), I will accept it. – Matthew May 22 '13 at 03:06
  • The intro from Stephen Cleary is the best async/await intro I have read so far. His next article [Best Practices in Asynchronous Programming](http://msdn.microsoft.com/en-us/magazine/jj991977.aspx) from the Next Steps section is also a must read. – Alex Dec 17 '13 at 12:19

2 Answers2

2

if no additional threads are involved, how can Produce yield control in the middle of the for loop?

Who said no additional threads are involved? The fact that you stated was:

The async and await keywords don't cause additional threads to be created.

Which is absolutely true. Your program includes the fragments

target.Post(i);

await source.OutputAvailableAsync())

My guess would be that the call to target.Post(i) or source.OutputAvailableAsync() created a thread. The await doesn't produce a thread; all the await does is assigns the remainder of the method as the continuation of the task returned by the call and then returns control to the caller. If that task spawned a thread to do its work, that's it's business.

await is just another control flow; a very complicated control flow, to be sure, but a control flow nevertheless. It's not a syntactic sugar for creating threads; it's a syntactic sugar for assigning a continuation to a task.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 4
    Actually, neither of those calls create threads. @Servy is correct in that the `await` operator (more specifically, the task awaiter used by the code generated by the `await` operator) is using the default `SynchronizationContext` to schedule method continuations on thread pool threads. – Stephen Cleary May 21 '13 at 22:18
  • @StephenCleary: Ah, good one. Thanks for the note. – Eric Lippert May 21 '13 at 23:11
-1

The control handling is done within the same thread, so that when the loop is running, the Consume method is not, and vice versa. If you were using threads it might not necessarily be so, and in fact you'd expect both to run concurrently.

The fact that they are in the same thread does not mean in any way that control can't pass from a part of the code to another. .NET (and every other framework, AFAIK) handles that smoothly, and each part runs with its own context with no problems.

However, running both things in one thread means that while Consume is running, the loop will "hang". If Consume takes too long, that's exactly what a user might perceive. That's why many programmers who are new to windows forms get surprised that filling their GUI controls with too much information at once causes their forms to hang and sometimes blank out - the thread that refreshes the screen is the same one where control logic runs, if you're not using background worker threads.

Geeky Guy
  • 9,229
  • 4
  • 42
  • 62
  • What causes control to pass to `Consume` without `Produce` explicitly yielding control? Does .NET notice that `Produce` has been running for a long time, and give control to `Consume` (similar to the role of the OS in a threaded model)? – Matthew May 21 '13 at 21:00
  • Actually I think Servy's got an answer better than mine in the comments. – Geeky Guy May 21 '13 at 21:12