0

Code:

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SampleAsyncConsoleProgram
{
    class Program
    {
        public static void ConsolePrint(string line)
        {
            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " ["
                + Thread.CurrentThread.ManagedThreadId.ToString() + "] > " + line);
        }

        static readonly IEnumerable<string> s_urlList = new string[]
        {
            "website1",
            "website2",
            "website3",
            "website4"
        };

        static Task Main() => DownloadWebsites();

        static async Task DownloadWebsites()
        {
            ConsolePrint("Main program: Program started..");

            var stopwatch = Stopwatch.StartNew();

            ConsolePrint("Main program: Adding tasks to list..");
            
            IEnumerable<Task> downloadTasksQuery =
                from url in s_urlList
                select ProcessUrlAsync(url);

            List<Task> downloadTasks = downloadTasksQuery.ToList();

            ConsolePrint("Main program: Added tasks to list.."); 

            while (downloadTasks.Any())
            {
                ConsolePrint("Main program: Checking if a task is completed.... followed by await...");
                Task finishedTask = await Task.WhenAny(downloadTasks);
                ConsolePrint("Main program: A task was completed..");
                downloadTasks.Remove(finishedTask);
                await finishedTask;
            }

            stopwatch.Stop();

            ConsolePrint($"Main program: Program Completed Elapsed time: {stopwatch.Elapsed}\n Current time is " + DateTime.Now);
        }

        static async Task ProcessUrlAsync(string url)
        {
            ConsolePrint("Task: Starts downloading " + url);
            await Task.Delay(5000); //represents async call to fetch url
            
            ConsolePrint("Task: Sleeping for 10 sec.." + url);
            Thread.Sleep(10000); //represents some long running blocking synchronous work and keeping thread busy...
            ConsolePrint("Task: Wake up.." + url);
            ConsolePrint("Task: Done Task.." + url);
        }
    }
}

Output:

13:39:24.567 [1] > Main program: Program started..
13:39:36.255 [1] > Main program: Adding tasks to list..
13:39:37.177 [1] > Task: Starts downloading website1
13:39:43.241 [1] > Task: Starts downloading website2
13:39:43.242 [1] > Task: Starts downloading website3
13:39:43.242 [1] > Task: Starts downloading website4
13:39:43.243 [1] > Main program: Added tasks to list..
13:39:43.243 [1] > Main program: Checking if a task is completed.... followed by await...
13:39:48.811 [5] > Task: Sleeping for 10 sec..website1
13:39:48.810 [7] > Task: Sleeping for 10 sec..website2
13:39:48.810 [4] > Task: Sleeping for 10 sec..website4
13:39:48.810 [6] > Task: Sleeping for 10 sec..website3
13:39:58.823 [4] > Task: Wake up..website4
13:39:58.826 [5] > Task: Wake up..website1
13:39:58.823 [6] > Task: Wake up..website3
13:39:58.826 [7] > Task: Wake up..website2
13:39:58.828 [5] > Task: Done Task..website1
13:39:58.828 [6] > Task: Done Task..website3
13:39:58.829 [7] > Task: Done Task..website2
13:39:58.828 [4] > Task: Done Task..website4
13:39:58.922 [7] > Main program: A task was completed..
13:39:58.923 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.923 [7] > Main program: A task was completed..
13:39:58.923 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.924 [7] > Main program: A task was completed..
13:39:58.924 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.924 [7] > Main program: A task was completed..
13:39:58.985 [7] > Main program: Program Completed Elapsed time: 00:00:22.6686293
 Current time is 30/07/2021 13:39:58

I was expecting that - at at line 65 (in the code), Thread.Sleep(10000); - each task should have independently blocked for 10 seconds (because I have used Thread.Sleep(10000) which is a synchronous and blocking code).

However from the output above, it looks like the Thread.Sleep (a blocking operation) is happening as just like multi-threading.

I understand that - async+await without Task.Run() in the case of windows app uses the same thread (UI thread). And that Thread.Sleep is a synchronous blocking operation.

  1. So why is each Task not blocking for 10 seconds? And what would be the way to make each Task block for 10 seconds?

  2. I have been advised that this behaviour is related to synchronization context. So it pulls threads from the thread pool (unlike the case of windows forms app). What I want to ask then is that - does this mean that it will perform multi-threading (multiple threads running in parallel)?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
variable
  • 8,262
  • 9
  • 95
  • 215
  • Change their order and you will see the difference. – Guru Stron Jul 30 '21 at 12:35
  • "If there are no other threads of equal priority that are ready to run, execution of the current thread is not suspended." https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.sleep?view=net-5.0 – user3026017 Jul 30 '21 at 12:38
  • 4 websites 5 seconds Thread.Sleep() each, shouldn't that be 20 seconds instead of 10? – Thomas Weller Jul 30 '21 at 12:41
  • 1
    _"I understand that - async+await without Task.Run() does not use multiple threads - so the above code uses only 1 thread"_ Your understanfding is a little bit off. It will not use threads to wait for IO-bound operations, but it will to run the continuations. – Guru Stron Jul 30 '21 at 12:42
  • Because you don't block the thread (don't get Threads and Tasks mixed up, they are different) until `await Task.WhenAny(downloadTasks);` The scheduler will attempt to run as many Tasks as it can (`await Task.Delay(5000);` is not blocking the thread so it will be released). Hence it will run ALL the `Task.Delay(5000)` at once (async) – Liam Jul 30 '21 at 12:48
  • Essentially your getting async and multi threading mixed up. There is only ever one thread running at any one time here. – Liam Jul 30 '21 at 12:49
  • 3
    Hey, please don't change your code all the time. People are working on answers. What does it look like if I post an answer for your code that was deleted? – Thomas Weller Jul 30 '21 at 12:50
  • 1
    The reason that you got the same thread name is that the thread was captured here: `Thread thr = Thread.CurrentThread;` and subsequently you accessed that thread's name instead of the thread that was actually running. – Thomas Weller Jul 30 '21 at 12:52
  • why don't you just use `Parrallel.ForEach`? – Liam Jul 30 '21 at 13:00
  • *console app doesn't have synchronization context* all Tasks have a synchronisation context. Not really sure what your getting at here? – Liam Jul 30 '21 at 13:05
  • *"So why is each Task not blocking for 10 seconds? And what would be the way to make each Task block for 10 seconds?"* <= Actually each `Task` does block a thread for 10 seconds, because of the `Thread.Sleep(10000)` in the implementation of the `ProcessUrlAsync` method. Did you meant to ask why all `Task`s don't block **the same thread** consecutively? – Theodor Zoulias Jul 30 '21 at 15:59
  • I'm confused because I had expected it to run on the same thread, from my knowledge of async await. But it looks like that is applicable only to windows forms due to the synchronization context concept. And in console app I don't know why it runs in parallel when async await is not same as parallelism. I didn't get this bit. – variable Jul 30 '21 at 16:15
  • Related: [Why a simple await Task.Delay(1) enables parallel execution?](https://stackoverflow.com/questions/62832993/why-a-simple-await-task-delay1-enables-parallel-execution) There are only a couple of answers worth reading there though. – Theodor Zoulias Jul 30 '21 at 16:36

2 Answers2

4

await Task.Delay(5000); does a quite good job of simulating IO-bound operations. And though IO-bound operations do not use threads to wait for completion (see There is no thread article by Stephen Cleary, also docs can shed some light), the continuations will be run on thread pool. So downloadTasksQuery.ToList() will start all your await Task.Delay's in parallel then (depending on number of tasks and thread pool and SynchronizationContext) settings some or all of them can be continued on separate threads.

So why is each Task not blocking for 10 seconds? And what would be the way to make each Task block for 10 seconds?

It blocks but it blocks a separate thread in your case.

I have been advised that this behaviour is related to synchronization context.

Yes, this behaviour can be affected by synchronization context. For example in desktop apps the continuations which are not marked with ConfigureAwait(false) will run on the single UI threads and since you don't have ConfigureAwait(false) configured for await Task.Delay(5000) you effectively end up making UI unresponsive.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • *or all of them* this will never use more than one thread. To start a new Thread the OP would need to use `Task.Run`, any CPU bound code will be ordered depending on that threads availability. – Liam Jul 30 '21 at 12:50
  • 3
    @Liam: the continuations will run on different threads – Thomas Weller Jul 30 '21 at 12:55
  • Yes, but there won't be multiple of them running at any one time. – Liam Jul 30 '21 at 12:55
  • 2
    @Liam: why not? I have a 8 core CPU, so if Windows wants to, it can run 8 in parallel – Thomas Weller Jul 30 '21 at 12:56
  • Because these are async calls and they are CPU bound not IO bound – Liam Jul 30 '21 at 12:57
  • That and the OP (though attempting to run in parrallel) keeps blocking by adding `await` all over the code. It looks like it'll run in parallel, but I'm pretty sure it won't. It's very confusingly written. – Liam Jul 30 '21 at 13:00
  • @Liam it seems that those are different threads and they are [suspended](https://dotnetfiddle.net/qOPM7p) simulating the CPU-bound work. And the first call is IO-bound (`Task.Delay`) – Guru Stron Jul 30 '21 at 13:00
  • Yes the threads are suspended and yes when they unsuspend, they may well get a different thread number. But the synchonisation context means they won't run in parallel. This is async code, not parrallel – Liam Jul 30 '21 at 13:02
  • @Liam but 1) console apps do not have synchonisation context by default. 2) And in this particular case the parallel processing is actually hapenning. – Guru Stron Jul 30 '21 at 13:07
  • 2
    @Liam: IMHO you're wrong this time. I confirmed it in WinDbg. There are 4 threads running, sorry. – Thomas Weller Jul 30 '21 at 13:10
  • I'd be interested to see a document that says "Console apps don't have synchgonisation contexts" It's been a while since I've written one but [from my research they do?](https://stackoverflow.com/a/52686950/542251) – Liam Jul 30 '21 at 13:12
  • [*if a thread’s current SynchronizationContext is null, then it implicitly has a default SynchronizationContext.*](https://learn.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext#the-implementations-of-synchronizationcontext) – Liam Jul 30 '21 at 13:15
  • so it's the default, that's not the same as *it does not have one* – Liam Jul 30 '21 at 13:15
  • 1
    @Liam: If I replace the Thread.Sleep by something to keep the CPU busy, it will use 16 cores at 100%: https://i.stack.imgur.com/FPvnp.png – Thomas Weller Jul 30 '21 at 13:15
  • Well I guess I'm wrong. I wouldn't of expected this but I don't often write code like this in console apps. I'm mainly a web dev and I know the Sync context in Asp.Net is very different. – Liam Jul 30 '21 at 13:18
  • https://xkcd.com/386/ – Liam Jul 30 '21 at 13:22
  • Can you comment on the 2 points that I have at the end of the question please? – variable Jul 30 '21 at 13:27
  • @Liam AFAIK having default `SynchronizationContext` is the same as not having one. I've seen [an issue with Ignite](https://gist.github.com/ptupitsyn/666ed575a350f18b3ae3cff641527fb6) cause runtime code is full of checks like [`if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))`](https://github.com/dotnet/runtime/search?q=typeof%28SynchronizationContext%29) – Guru Stron Jul 30 '21 at 13:27
  • @variable please see the update. Also can you explain what are you trying to achive? – Guru Stron Jul 30 '21 at 13:51
  • @Liam the [`Task.Run`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run) doesn't start a new thread. It schedules work on the reusable `ThreadPool` threads. – Theodor Zoulias Jul 30 '21 at 16:10
  • Guru can you comment on - what would be the way to make each Task use same thread? – variable Jul 30 '21 at 16:27
  • @variable it depends on why do you need to do that. You can run this code in the UI thread of desktop app which will have the same effect. – Guru Stron Aug 02 '21 at 14:32
  • "the continuations will be run on thread pool" unless one uses .ConfigureAwait(false) even in console apps, right? – AMG Aug 22 '23 at 15:20
  • @AMG If I understand your question correctly then it is the other way around. `ConfigureAwait(false)` prevents capturing current synchronization context/scheduler which results in `TaskScheduler.Default` used which is thread pool one. – Guru Stron Aug 22 '23 at 15:31
  • 1
    In the presence of `ConfigureAwait(false)`, the `await` continuation runs on the thread that completed the `Task`. In practice this is almost always a `ThreadPool` thread, but in theory it can be any thread ([demo](https://dotnetfiddle.net/TZYlDQ)). – Theodor Zoulias Aug 22 '23 at 15:42
3

This answer is based on an older version of your code. The reason that it always printed the same thread name is that the thread was captured and thus thr.Name was not the name of the current thread but the captured thread instead.

If you inline that variable and use the managed thread ID, you can see that there are mutiple threads inside the sleep:

static async Task ProcessUrlAsync(string url)
{
    Console.Write(Thread.CurrentThread.ManagedThreadId + " starts downloading " + url + " at " + DateTime.Now + "\n\n");
    await Task.Delay(5000); //represents async call to fetch url
    Console.Write(Thread.CurrentThread.ManagedThreadId + " sleeping for 5 sec.. " + url + " at " + DateTime.Now + "\n\n");
    Thread.Sleep(5000); //represents some long running blocking syncronous work and keeping thread busy...
    Console.Write(Thread.CurrentThread.ManagedThreadId + " sleeping done for.. " + url + " at " + DateTime.Now + "\n\n");
    Console.Write(Thread.CurrentThread.ManagedThreadId + " - Done " + url + " at " + DateTime.Now + "\n\n");
}

Partial output:

5 sleeping for 5 sec.. website3 at 30.07.2021 14:54:09
7 sleeping for 5 sec.. website1 at 30.07.2021 14:54:09
4 sleeping for 5 sec.. website4 at 30.07.2021 14:54:09
6 sleeping for 5 sec.. website2 at 30.07.2021 14:54:09

You can also see 4 threads being in the same method using VS parallel stack view.

Screenshot

And in WinDbg, you can see that 4 OS threads have been assigned:

0:000> ~*e!clrstack
[...]
OS Thread Id: 0x455c (11)
Child SP       IP Call Site
0665f444 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x35b4 (12)
Child SP       IP Call Site
0681f1f4 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x17e4 (13)
Child SP       IP Call Site
0695f3e4 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x475c (14)
Child SP       IP Call Site
06a9f124 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Thomas Weller
  • 55,411
  • 20
  • 125
  • 222