Sam
Sam

Reputation: 4484

How to know the number of Threads created and limit the Tasks accordingly

It is pretty clear that using Task with async/await rather than Thread is the way to go for making asynchronous calls. My question is that is there a way to monitor the threads that are spawned while completing these Tasks? This is so I can decide an optimal number of Tasks to schedule such that the threads doesn't eat up a lot of CPU cycles at once (assuming the Tasks are CPU intensive).

Let's take an example below (output is mentioned too). Though the program completes in 5 secs, it has created two threads (Id=1,4) to complete all tasks. If I increase the number of tasks to 6 instead of 2, it creates 4 threads. I am aware that these CLR threads map to OS threads (which are total 4 in my machine), but I would like to know how are they getting mapped (along with tasks) and corresponding CPU utilization. Is there a way to achieve this?

TEST CODE

    static void Main(string[] args)
    {
        RunTasksWithDelays().Wait();
    }

    static async Task RunTasksWithDelays()
    {
        Stopwatch s = Stopwatch.StartNew();

        Console.WriteLine("ThreadId=" + Thread.CurrentThread.ManagedThreadId);
        Task task1 = LongRunningTask1();
        Task task2 = LongRunningTask2();

        await Task.WhenAll(task1, task2);
        Console.WriteLine("total seconds elapsed:  " + s.ElapsedMilliseconds/1000);
    }

    static async Task LongRunningTask1()
    {
        Console.WriteLine("1 start " + DateTime.Now);
        Console.WriteLine("ThreadId=" + Thread.CurrentThread.ManagedThreadId);
        await Task.Delay(5000);
        Console.WriteLine("ThreadId=" + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine("1 end " + DateTime.Now);
    }

    static async Task LongRunningTask2()
    {
        Console.WriteLine("2 start " + DateTime.Now);
        Console.WriteLine("ThreadId=" + Thread.CurrentThread.ManagedThreadId);
        await Task.Delay(2000);
        Console.WriteLine("ThreadId=" + Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine("2 end " + DateTime.Now);
    }

OUTPUT

ThreadId=1
1 start 28-10-2014 18:27:03
ThreadId=1
2 start 28-10-2014 18:27:03
ThreadId=1
ThreadId=4
2 end 28-10-2014 18:27:05
ThreadId=4
1 end 28-10-2014 18:27:08
total seconds elapsed:  5
Press any key to continue . . .

Upvotes: 1

Views: 4085

Answers (3)

George Polevoy
George Polevoy

Reputation: 7681

TPL does not create threads as you spawn tasks, it's the ThreadPool which does.

Assumption that there are 4 system threads on a 4-CPU machine is wrong. There are maximum 4 threads that has their time slice scheduled at the same time on a 4-CPU processing machines, but the thread objects number in a system is much higher. These objects are expensive to create and maintain.

More thread pool threads would be created in case your threads are sleeping while making a blocking call, and CPU usage is desaturated. Then, if you have queued items on ThreadPool, it makes it's best to saturate processor, creating more threads.

If you want to exercise the ThreadPool actually creating more threads in a test, you should use something CPU-saturating, such as a while(counter-- > 0);. Only then you would get more threads created. await Task.Delay(2000); does not need more threads to process, because no items are actually queued to the ThreadPool, and there are always enough threads in the pool waiting to process the queue.

If you do it right, there is a chance all your non-cpu-intensive tasks (io-bound, for example) processing ends up processed on one or two threads which is good, and really is the purpose of async processing.

Following code is cpu-intensive, and always prints same number of managed threads as processor count on my computer:

    static void Main(string[] args)
    {
        RunTasksWithDelays();
    }

    static void RunTasksWithDelays()
    {
        var s = Stopwatch.StartNew();
        var tasks = Enumerable.Range(0, 50).Select(i => LongRunningTask()).ToArray();

        // Don't need explicit wait, .Result does it effectively.
        Console.WriteLine(tasks.SelectMany(t => t.Result).Distinct().Count());
        Console.WriteLine(s.Elapsed);
        Console.WriteLine(Environment.ProcessorCount);
    }

    static async Task<List<int>> LongRunningTask()
    {
        await Task.Yield(); // Force task to complete asyncronously.
        var threadList = new List<int> {Thread.CurrentThread.ManagedThreadId};
        var count = 200000000;
        while (count-- > 0) ;
        threadList.Add(Thread.CurrentThread.ManagedThreadId);
        return threadList;
    }

Upvotes: 0

i3arnon
i3arnon

Reputation: 116636

These async methods don't create new threads. They use threads from the ThreadPool (by default. You can specify otherwise). If you want to know how many threads that pool holds you can use ThreadPool.GetMaxThreads:

int workerThreads;
int completionPortThreads;
ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
Console.WriteLine(workerThreads);

When GetMaxThreads returns, the variable specified by workerThreads contains the maximum number of worker threads allowed in the thread pool, and the variable specified by completionPortThreads contains the maximum number of asynchronous I/O threads allowed in the thread pool.

You can use the GetAvailableThreads method to determine the actual number of threads in the thread pool at any given time.

About the degree of parallelism, the optimal number for truly CPU-intensive work is usually the amount of logical cores the machine has, because more threads would just increase redundant context switches

Upvotes: 0

Stephen Cleary
Stephen Cleary

Reputation: 457117

using Task with async/await rather than Thread is the way to go for making asynchronous calls... (assuming the Tasks are CPU intensive).

Asynchronous (usually I/O-bound) tasks are not CPU-intensive. So you don't have to worry about it.

If you are doing CPU-intensive work, look into Parallel/Parallel LINQ or TPL Dataflow, both of which have built-in options for throttling. TPL Dataflow in particular is nice for mixing I/O and CPU-intensive code.

Upvotes: 3

Related Questions