-1

I'm here because I'm having a weird behaviour using this code : But before that, I KNOW THAT IS A REALLY BAD PRACTICE TO DO THAT, so it's not even used in reality I just want to understand what is happenning behind the scene but my knowledges are really poor about that.

Here is the code in question :

int worker = 0;
int io = 0;
Console.WriteLine($"Worker thread {worker} Io thread {io}");
ThreadPool.GetAvailableThreads(out worker, out io);
ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax);

Console.WriteLine($"Worker thread {workerThreadsMax - worker} Io thread {completionPortThreadsMax - io}");
for (int i = 0; i < 100; i++)
{
                
    Task.Run(() =>
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " - Running thread");
        ThreadPool.GetAvailableThreads(out var worker2, out var io2);
        ThreadPool.GetMaxThreads(out var workerThreadsMax2, out var completionPortThreadsMax2);
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Worker thread {workerThreadsMax2 - worker2} Io thread {completionPortThreadsMax2 - io2}");

        var t1 = Task.Delay(5000);
        var t2 = Task.Delay(5000);
        Task.WaitAll(t1, t2);

        Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " - End of thread");
        ThreadPool.GetAvailableThreads(out worker2, out io2);
        ThreadPool.GetMaxThreads(out workerThreadsMax2, out completionPortThreadsMax2);
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Worker thread {workerThreadsMax2 - worker2} Io thread {completionPortThreadsMax2 - io2}");
    });
}

Console.ReadLine();

So what I'm trying to do in this code is to run or at least queue 500 tasks (that a lot I know, but was curious), while still displaying the number of active Threads from the ThreadPool. So in the first line I have 0 worker thread in the ThreadPool which makes sense, as long as I didn't start any Task yet. But when the first Task runs there is 8 active Threads. And here is where a weird thing is happening : A new thread is spawned every second or less (but it's not instantly), which is not a real issue but the thing I don't understand is why the Task are blocked ? Even when the 250ms delay are finished the Task don't end itself, it still blocked on the Task.WaitAll line even after more than one minute :

Worker thread 0 Io thread 0
Worker thread 0 Io thread 0
8 - Running thread
8 - Worker thread 8 Io thread 0
6 - Running thread
6 - Worker thread 8 Io thread 0
10 - Running thread
10 - Worker thread 8 Io thread 0
7 - Running thread
7 - Worker thread 8 Io thread 0
11 - Running thread
11 - Worker thread 8 Io thread 0
5 - Running thread
9 - Running thread
9 - Worker thread 8 Io thread 0
12 - Running thread
12 - Worker thread 8 Io thread 0
5 - Worker thread 8 Io thread 0
13 - Running thread
13 - Worker thread 9 Io thread 0
14 - Running thread
14 - Worker thread 10 Io thread 0
15 - Running thread
15 - Worker thread 11 Io thread 0
16 - Running thread
16 - Worker thread 12 Io thread 0
17 - Running thread
17 - Worker thread 13 Io thread 0
18 - Running thread
18 - Worker thread 14 Io thread 0

Is there any deadlock happening here ? If can someone can explain me this, it would be really great. Thanks.

EDIT : For those who proposed to use async and await Task.WhenAll(..) you are totally right ! But as I said, it was for testing purpose and i won't do that in the reality and use instead the async/await statements for that. But we were testing something with a friend about synchronous and asynchronous Task and when testing the synchronous way we came across this problem withotu knowing what was happening. Thank you for those who clarified this. It has been very instructive.

Shiglet
  • 101
  • 9
  • 1
    It should be `await Task.WaitAll(....)`. You need to make your method `async` to release the thread – Liam Nov 16 '21 at 14:08
  • Spawing two tasks seems like an odd thing to do as well... – Liam Nov 16 '21 at 14:08
  • 2
    @Liam `WaitAll` is not awaitable overload AFAIK. – Guru Stron Nov 16 '21 at 14:09
  • 2
    Yes good shout, should be `WhenAll` – Liam Nov 16 '21 at 14:10
  • 1
    You are observing the effects of a saturated `ThreadPool`. You can experiment by adding this line at the start of the program: `ThreadPool.SetMinThreads(50, 50)`, and see the difference in behavior. – Theodor Zoulias Nov 16 '21 at 14:16
  • 3
    Btw inside the question you are talking about "500 tasks", but the loop is `for (int i = 0; i < 100; i++)`. You are also talking about "250ms delay", but the only delay I can see is `Task.Delay(5000)`. – Theodor Zoulias Nov 16 '21 at 14:18
  • 3
    It is the opposite of deadlock, you see the threadpool manager trying to work around these threads not finishing in a timely manner and getting stuck on the WaitAll() call. It doesn't otherwise know why they don't make progress and assumes it *might* be caused by deadlock, allowing an extra thread to run could solve the problem. You're supposed to use the TaskCreationOptions.LongRunning option for tasks that take more than half a second. With the side-effect that it now uses a regular Thread instead of a tp thread and is no longer throttled by the pooling rules. – Hans Passant Nov 16 '21 at 14:58
  • 1
    @TheodorZoulias yeah my bad there, i did a bad copy paste. It should be 250 ms and 500 in the loop. But the result is still the same even i change those parameters. Also you first answer seems to join the answer Jonas proposed. Thank you all guys. I understand better now. – Shiglet Nov 16 '21 at 15:49
  • Shiglet could you [edit](https://stackoverflow.com/posts/69990578/edit) the question and fix these inconsistencies? – Theodor Zoulias Nov 16 '21 at 15:51

2 Answers2

6

Lets look at this piece of code in greater detail

var t1 = Task.Delay(5000);
var t2 = Task.Delay(5000);
Task.WaitAll(t1, t2);

Task.Delay is essentially a wrapper around a timer, more specifically a System.Threading.Timer. This timer will delegate the actual time keeping to the OS. When the timer has elapsed it will raise the even on a thread pool thread that in turn marks the task as completed. This will trigger a check for the Task.WhenAll task to see if that can complete, and if so unblock.

However, your test is essentially designed to exhaust the threadpool, Causing a classic deadlock. All the Task.WhenAll tasks are waiting for one or more Task.Delay to complete, but this requires an available threadpool thread, but all threads are blocked while waiting for the the Task.WhenAll tasks. So everything is waiting for something else, and nothing can run.

Except the designers of the threadpool anticipated this problem and added a mechanism that increases the number of threadpool threads, allowing the Task.Delay to complete, and resolve the deadlocks. But this mechanism is slow. So it should not be a surprise that there is a huge delay for tasks to complete.

Since this is a toy example a solution might not be required, but it might be worth repeating. Do not oversubscribe the threadpool. Use asynchronous, non-blocking code, or code that is careful about how many threads it uses.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Perfect answer ! I thought there was something like this happening behind the scene but you added so much details and informations. Yeah sure i agree with you last sentences, it was just for testing purposes we wwere doing some tests (with async and non async way) with a friend and when trying sync way we got that "issue" and couldn't explain it as you did. Thank you. – Shiglet Nov 16 '21 at 15:47
0

Your Task should be:

Task.Run(async () =>
    {


        var t1 = Task.Delay(5000);
        var t2 = Task.Delay(5000);
        await Task.WhenAll(t1, t2);


    });

Without the async, the method is blocking. The async/await makes the task non-blocking. As mentioned by Gurustron you need to use the non-blocking Task.WhenAll not, Task.WaitAll. See WaitAll vs WhenAll

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Liam
  • 27,717
  • 28
  • 128
  • 190