4

I'm following an article showing how async/tasks are not threads. https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

In their example they build up an async call showing this by creating 3 tasks that each run a loop that pauses for a number of seconds. I've recreated it in a console (marked as STAThread) application.

However, in my code below I was expecting it to take the same 15 seconds (5 seconds for each task) in both examples as stated in the article. However, it is only taking 5 seconds for the second and so is running all 3 at the same time resulting in 5 seconds to complete for my second example. The original article took 5 seconds but I changed it to a 1 second delay to make it more obvious.

Can anyone explain the what is going on and why it is running seemingly threaded?

    class AsyncTest
    {
        Stopwatch sw = new Stopwatch();
        internal void Tests()
        {
            DoASynchronous1();
            //update below to be DoAsync2 and repeat
        }

        private void DoASynchronous1()
        {
            sw.Restart();
            var start = sw.ElapsedMilliseconds;
            Console.WriteLine($"\nStarting ASync Test, interval 1000ms");
            Task a=DoASync1("A",1000);     //change these to 2           
            Task b=DoASync1("B",1000);                
            Task c=DoASync1("C",1000);                
            Task.WaitAll(a, b, c);
            sw.Stop();
            Console.WriteLine($"Ended Sync Test. Took {(sw.ElapsedMilliseconds - start)} mseconds");
            Console.ReadKey();
        }

        //this runs synchronously showing async is not threaded
        //this takes 15 seconds
        private async Task DoASync1(string v, int delay)
        {
            //loop for 5 seconds
            for (int i = 1; i <= 5; i++)
            {
                await Task.Delay(0); //or simply omit
                var startTime = sw.ElapsedMilliseconds;
                while (sw.ElapsedMilliseconds < startTime+(delay)) { }
                Console.WriteLine($"{v}:{i}");
            }
        }

        //this is taking 5 seconds
        private async Task DoASync2(string v, int delay)
        {
            //loop for 5 seconds
            for (int i = 1; i <= 5; i++)
            {
                await Task.Delay(100);
                var startTime = sw.ElapsedMilliseconds;
                while (sw.ElapsedMilliseconds < startTime + delay) { }
                var endtime = sw.ElapsedMilliseconds;
                Console.WriteLine($"{v}:{endtime}");
            }
        }

    }
}
Neil Walker
  • 6,400
  • 14
  • 57
  • 86
  • check out this so post for a good explanation https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading – Daniel Feb 18 '19 at 13:46
  • Thanks, I've read how it all works (I actually have that tab open already), I'm asking why the example async is running 3 tasks at the same time when according to the msdn article they should still be synchronous (and as I haven't introduced a .Run() to make it threaded)... – Neil Walker Feb 18 '19 at 13:47
  • @NeilWalker - Why do you think that they should be synchronous? – Enigmativity Feb 18 '19 at 13:49
  • Well, that's a good question. I presumed this task was cpu bound and there was no IO so without a .Run it would all be on the same thread as I haven't told it to ConfigureAwait(false). Also, this MSDN article explicitly tells me this, but fair enough it's a UI applicaiton. Is my console code fundamentally different to the windows form/wpf? – Neil Walker Feb 18 '19 at 13:51
  • @NeilWalker - Imagine a chef cooking a steak, some pasta, and a sauce. He heats the fry pan, boils water in a pot, and stirs his tomato sauce, drop in the pasta to the pot, and throws on the steak. He drains the pasta, flips the steak, and seasons his sauce. There was no need to call. `Task.Run(() => new Chef())`. The one chef was able to do all of these things at the same time by just jumping between the steps of each dish. – Enigmativity Feb 18 '19 at 13:53
  • Thanks, I really do get that I'm just trying to understand IO/CPU bound tasks (as in IO goes to the kernel to do it's IO), and I read that article you mentioned last week. What I'm asking is why in the MSDN article is it doing my second async synchronously whereas my code is not. My code has explicit 5 second delay for each job. Is it because it is a single threaded WPF? If so I presumed adding [STAThread] would do the same thing. The only difference is the Delay(x) statement. I guess to add to that, when do you need to call Run then if not a cpu intensive task? – Neil Walker Feb 18 '19 at 13:57
  • I also don't understand why the thread spinning (or as I have tried, `Thread.Sleep`) runs in parallel even though it's not part of an `await`. – Rawling Feb 18 '19 at 13:59
  • Did you try to output `Thread.CurrentThread.Name` ? – Fildor Feb 18 '19 at 14:01
  • 2
    I think the key here is that in the absence of a captured synchronisation context (which would exist on the WPF main thread, for example), after `Task.Delay` execution will resume on a thread pool thread. The result of that is that all 3 tasks will end up doing their 'work' (spinning) on different threads. – Charles Mager Feb 18 '19 at 14:06
  • STA is a *consequence* /side effect of being on the UI thread. It's not the *definition* of the UI thread. STA itself mostly matters for COM purposes. – Damien_The_Unbeliever Feb 18 '19 at 14:09
  • Ok, so I ran it within a Windows form and it works as per MSDN (15secs), and as a STA console it is 5secs so I guess it's as per what Charles/Damien say. I was hoping to learn all this to build up to showing colleagues how await works and presumed from what I've read that it would not use any kind of thread pool (I even set ConfigureAwait(true) and so not entirely sure what different doing 'cpu' bound without a Run() makes to when using a Run() as my code kind of shows it runs 'async' across all 3 tasks anyway :( – Neil Walker Feb 18 '19 at 14:21

1 Answers1

4

The key here is the absence of a synchronisation context. The linked article is a WPF application, which has a synchronisation context on its Main/UI thread. The console app you've created won't have one at all.

The result of this is that where the article says e.g.

When you “await” an asynchronous task, the rest of your method will continue running in the same context that it started on. In WPF, that context is the UI thread.

That doesn't apply to your example. After Task.Delay, your code will resume on a thread pool thread. This allows all 3 of your tasks to run in parallel.

This is much the same as when the article later uses ConfigureAwait(false), which prevents resuming execution on the captured synchronisation context.

Charles Mager
  • 25,735
  • 2
  • 35
  • 45
  • 1
    @Bradley That's not Jon Skeet's blog that you linked to... Wrong link, or wrong name? – Cody Gray - on strike Feb 18 '19 at 14:30
  • Right link, wrong name. I included John's name in the google search, and assumed that the result was from his blog. It's still a good source of information though. – Bradley Uffner Feb 18 '19 at 14:31
  • Since I got the blog name wrong in my first comment, and it was too late to correct it, I'll repost it directly: https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/ – Bradley Uffner Feb 18 '19 at 14:33
  • Thanks. So if this is essentially using the thread pool, what difference is there between the code I wrote and wrapping my loop inside a Run method as Run says it 'Queues the specified work to run on the thread pool and returns a Task'? Knowing this, I will be an async master ;-) – Neil Walker Feb 18 '19 at 16:34
  • @NeilWalker It would change the thread the code before the first `Task.Delay` runs on. In terms of perceptible behaviour, it wouldn't change anything in this console app. You may find it useful to include `Thread.CurrentThread.ManagedThreadId` in your log statements. – Charles Mager Feb 18 '19 at 16:37