2

I'm actually studying async/wait and trying to see for myself the benefit of await Task.WhenAll versus Task.WaitAll in CPU bound operations. As everyone write that Task.WaitAll provides a blocking wait while await Task.WhenAll provides a non-blocking wait.

I created an example in which I wanted to replace Task.WaitAll with an await Task.WhenAll and see with my own eyes that there was one more free thread. But I see that even Task.WaitAll does not block the thread. And my question is related to this. In the case of Task.WaitAll, I see that in the same thread in which Task.WaitAll is executed, another task is being executed. But if I include Thread.Sleep or while (true) instead of Task.WaitAll, then the behavior of the program becomes as expected.

I thought that the Main method will create task MyTask (-1 worker thread), which will create 16 tasks conditionally B1-B16 (-15 worker threads since 1 worker thread is busy with task MyTask, and there are 16 worker threads in total), task MyTask will have a blocking wait Task.WaitAll and I will see 15 out of 16 running tasks. But I see all 16 running tasks and one of them is running on the same thread that task MyTask is running on.

Question. Why does Task.WaitAll not block the thread in which it is executed in this example, unlike Thread.Sleep or while (true)? Can someone explain step by step how the code of two tasks in thread 4 works in case of using Task.WaitAll? Why is the thread in which task MyTask runs also used by task conditionally B16?

using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp1 { class Program { static void Main(string[] args) { Console.WriteLine($"Main Thread: {Thread.CurrentThread.ManagedThreadId}"); int ProcessorCount = Environment.ProcessorCount; ThreadPool.SetMaxThreads(ProcessorCount, ProcessorCount); int Counter = 0; List<Task> MyListForTask = new List<Task>(); void MyMethod() { lock (MyListForTask) { Counter++; Console.WriteLine($"Counter: {Counter} Thread: {Thread.CurrentThread.ManagedThreadId}"); } //Thread.Sleep(int.MaxValue); while (true) { }; } Task MyTask = Task.Run(() => { Console.WriteLine($"MyTask Thread: {Thread.CurrentThread.ManagedThreadId}\n"); for (int i = 0; i < ProcessorCount; i++) { MyListForTask.Add(Task.Run(MyMethod)); } //Thread.Sleep(int.MaxValue); //while (true) { }; Task.WaitAll(MyListForTask.ToArray()); }); MyTask.Wait(); } } } 

enter image description here

enter image description here

enter image description here

enter image description here

12
  • Check out this article by Stephen Toub. My guess is that the MyTask.Wait() is "stealing" work from one of the other tasks in the list. You might want to test this hypothesis by replacing the Task.Run with the Task.Factory.StartNew, and passing the TaskCreationOptions.PreferFairness option. Commented Nov 23, 2022 at 13:50
  • And this theft depends on whether I use Task.WaitAll or Thread.Sleep in MyTask? Sounds weird... I replaced MyTask.Wait() with Thread.Sleep(int.MaxValue), nothing changed. Commented Nov 23, 2022 at 14:26
  • Simply by using Thread.Sleep you make all observations invalid. Tasks aren't threads, they're essentially promises/job descriptions. Tasks run on reusable threadpool threads. Calling Thread.Sleep not only blocks those threads, it removes them from the OS's scheduling, forcing the runtime to create new ones. If you want to emulate waiting use Thread.SpinWait at least. Commented Nov 23, 2022 at 15:45
  • @PanagiotisKanavos, In debugging in the windows of tasks and threads, it is clearly seen that in the case of Thread.Sleep instead of Task.WaitAll 16 tasks are started and one is scheduled, in the case of Task.WaitAll instead of Thread.Sleep all 17 tasks are started and blocked. Does this refer to an invalid observation? I look not only at the console. One task did not have enough flow. Can you explain step by step how the code of the two tasks in thread 4 works in case of using Task.WaitAll? Commented Nov 23, 2022 at 16:13
  • If you want to see what's actually going on use the Concurrency Visualizer in VS. Using a file or the console to indirectly monitor threads isn't just slow, it also adds an unexpected synchronization. I'll repeat this though - tasks aren't threads. They're a promise that something will complete and maybe produce a value in the future. They may not even use a thread to complete. If they use a thread, that may or may not come from the thread pool. Commented Nov 23, 2022 at 16:14

1 Answer 1

2

The whole point of multithreading / asynchronous programming is to use your CPU resources as effectively as possible and you do not care about the order of operation.

There's no guarantee that the order the Tasks were started in, they will also be completed in.

Thread.Sleep, as the name implies, actively blocks the CPU thread (will not pick up another task) and waits until the required condition has been met (x time passed) before executing the task - and only then picking another task. In short, Thread.Sleep prevents asynchronous behavior from occuring.

Here you can see more intuitively what each one will do. Output will not be 1-100 consecutively, but random.

The WhenAll even prints the DoSomethingElse first since the Tasks are still starting/being executed.

The WaitAll will wait for the tasks before printing DoSomethingElse even though that slows down execution.

The Sleep, as mentioned, only adds time. You can put a thread to sleep in an async or 'sync' method, but the only thing it does is add execution time to your program. The only difference is that in an async other available threads in will pick up the slack (if available).

using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Fiddle { public class Program { public static void Main(string[] args) { var ladieDo = new DoSomething(); ladieDo.RunAsyncAndDontAwaitCompletion(); Console.ReadLine(); Console.Clear(); ladieDo.RunAsyncAndAwaitCompletionOfAll(); Console.ReadLine(); } public class DoSomething { public void RunAsyncAndDontAwaitCompletion() { var proces = Process.GetCurrentProcess(); Console.WriteLine("Threads:" + proces.Threads); var ints = Enumerable.Range(1, 100).ToList(); // Will report back when its done, but wont wait for everything Task.WhenAll(ints.Select(a => Task.Run(() => Console.WriteLine(a)))); // This line will be executed as soon as a Thread opens up, regardless of whether the above tasks have been completed Console.WriteLine("DoSomethingElse"); Console.ReadLine(); } public void RunAsyncAndAwaitCompletionOfAll() { var proces = Process.GetCurrentProcess(); Console.WriteLine("Threads:" + proces.Threads); var ints = Enumerable.Range(1, 100).ToList(); // wait untill all these tasks are done Task.WaitAll(ints.Select(a => Task.Run(() => Console.WriteLine(a))).ToArray()); // only once above tasks are done (regardless of order), write this Console.WriteLine("DoSomethingElse"); } public void EachTaskWillRunSynchronously() { var proces = Process.GetCurrentProcess(); Console.WriteLine("Threads:" + proces.Threads); var ints = Enumerable.Range(1, 100).ToList(); foreach (int i in ints) { Console.WriteLine(i); // The below line will just add more time in between each output //Thread.Sleep(10); } Console.WriteLine("DoSomethingElse"); } } } 

}

Output:

DoSomethingElse 1 2 3 4 6 7 8 9 10 11 12 13 14 5 .... 89 90 91 92 93 94 88 84 97 98 96 100 99 95 DoSomethingElse 
Sign up to request clarification or add additional context in comments.

5 Comments

Then what is the blocking wait for Task.WaitAll?
@NikVladi only after all the results are in, will it continue with the next line of code (in your case MyTask.Wait(). Since order of processing/output is not guaranteed in asynchronous calls, they are usually sorted afterwards (before being displayed).
In the first case, I see that the method from MyTask was launched in the 4th thread. Further, the method from MyTask creates new tasks, that is, the 4th thread should be occupied with just this. After that, in the method from MyTask in the 4th thread, Task.WaitAll should work, which should block the 4th thread? Am I right? Can you explain step by step how the code works by talking about thread 4? And how does it happen that in the 4th thread, which should always be busy, a method from another task is executed?
@NikVladi I'll write up a better example, give me a few min.
Thank you for your work, but I'm sorry, I don't understand at all what is the connection between your example and my question. My example clearly demonstrates that Thread.Sleep actually blocks the thread, but Task.WaitAll does not. How did I get it? I use Thread.Sleep and it works 15 tasks, and when I use Task.WaitAll instead it works 16 tasks. Means there is one more free stream. Why? Everyone writes that Task.WaitAll is a blocking wait. But the thread is not blocked. And I asked for a step by step explanation of how the code in my example works in thread 4.