Learn about default TaskScheduler and ThreadPool in .NET to avoid reducing performance of Task.Run drastically

Introduction

Task.Run method is introduced in .NET Framework 4.5, and it’s simple for us to use async/await keywords. Also, use this method can help us manage threads with ThreadPool, so we can write asynchronous codes as simple as synchronous codes.

But, if Task.Run method is abused, it will reduce the application’s performance drastically. In this article, we will introduce more details about the default thread pool task scheduler. And we will introduce the right way to use Task.Run, to avoid reducing the application’s performance.


Detail

The original link is 了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低. But this blog was written in Chinese, so I translate its content to English. Waterlv is an MVP(Microsoft Most Valuable Professional), and he is good at .NET Core\WPF\.NET. Here is his Blog.


How to use Task.Run method

  1. For the IO operation, developers should try their best to use Async method provided in IO class instead of using Task.Run to execute an IO operation. (Task.Run will take up thread pool resources.)
  2. If there are not any Async methods to finish a time-consuming operation of IO, we should set the value of CreateOptions to LongRunning.
  3. We recommend using Task.Run method to execute short-running tasks.

In the subsequent paragraphs, we will analyze the reasons for these usages posted above.

Demo

Before analyzing, we wrote a test application. In this application, it runs 10 asynchronous tasks with Task.Run method, and in each task, there is a Thread.Sleep(5000) method to suspend thread for 5 seconds.

It is easy to find that the starting time of these 10 tasks is different, even we run these tasks at the same time. The first 4 tasks started immediately, then it started a new asynchronous task every second.

Here is the code of this demo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Program
{
static async Task Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[Index] [ID] [Time]");
Console.ForegroundColor = ConsoleColor.White;

var task = Enumerable.Range(0,10).Select(i=>Task.Run(()=>LongTimeTask(i))).ToList();
await Task.WhenAll(task);

Console.Read();
}

private static void LongTimeTask(int index)
{
var threadId = Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(2, ' ');
var line = index.ToString().PadLeft(2, ' ');
Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] Asynchronous task started--------");

//Sleep 5 seconds
Thread.Sleep(5000);

Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] Asynchronous task ended-----------");
Console.ForegroundColor = ConsoleColor.White;
}
}

TaskScheduler

The reason for these tasks above couldn’t start right away is that the Task class use TaskScheduler to schedule threads. If developers don’t set the value of TaskScheduler, it will use ThreadPoolTaskScheduler as its default value. And, the Task.Run method uses the default scheduler of .NET, and you can get it by TaskCheduler.Default.

For more information, visit source code of .NET CoreThreadPoolTaskScheduler.QueueTask.

So, the setting in the thread pool will determine when to start a new thread to execute the task.

ThreadPool

Developers can get the minimum number of worker threads and the asynchronous ‘IO’ threads by ThreadPool.GetMinThreads method. Also, we can get their maximum number by Thread.GetMaxThreads method. And we can call the Set... methods to set the value of the minimum or the maximum number of threads.

For more details ,visit ThreadPool.GetMinThreads(Int32, Int32) Method (System.Threading) - Microsoft Docs

So, here are some items of how the ThreadPool works.

  • The thread pool creates new worker thread or IO thread on demand until it reaches the minimum number of each category.
  • By default, the minimum number of threads is the same as the number of processors on the computer.
  • After reaching the minimum value, the thread pool can create other threads or just wait until some tasks finished.
  • When the demand is low, the number of threads in the thread pool can be less than the minimum value.

That is why the tasks of the demo posted above can’t start at the same time. In my computer (4-processors), the minimum value of threads is 4, so the first 4 tasks can be executed immediately. When the number of thread reaches 4 and there are still no threads finished, the thread pool will try to wait for other tasks to complete. But, if there are still no threads finished after 1 second, the thread pool will create a new thread to execute the new task. But when there are some threads finished, it will use these threads to execute the new tasks instead of creating new threads.

But, it is important to notice that the number of threads should be less than the maximum value.

After learning about the default action of ThreadPoolTaskScheduler, we can take advantage of the thread pool by doing these things:

  1. For the IO operation, try to use the methods with Async prefix provided by IO class to occupy IO threads in the thread pool instead of normal threads( don’t use Task.Run to consume thread pool resources)
  2. If there are not any Async methods to finish a time-consuming operation of IO, we should set the value of CreateOptions to LongRunning.(After setting this value, it will create a new thread instead of using the thread in the thread pool)
  3. We recommend using Task.Run method to execute short-running tasks.

References