How is Task.Run limited by CPU cores?

2019-09-07 13:40发布

问题:

Why is it that the following program will only run a limited number of blocked tasks. The limiting number seems to be the number of cores on the machine.

Initially when I wrote this I expected to see the following:

  • Job complete output of Jobs 1 - 24
  • A 2 second gap
  • Output of Jobs 25 - 48

However the output was:

  • Job complete output of Jobs 1 - 4
  • Then randomly completing jobs every couple of 100ms.

When running on server with 32 cores, the program did run as I had expected.

class Program
{
    private static object _lock = new object();
    static void Main(string[] args)
    {
        int completeJobs = 1;
        var limiter = new MyThreadLimiter();
        for (int iii = 1; iii < 100000000; iii++)
        {
            var jobId = iii;
            limiter.Schedule()
                .ContinueWith(t =>
                {
                    lock (_lock)
                    {
                        completeJobs++;
                        Console.WriteLine("Job: " + completeJobs + " scheduled");
                    }
                });
        }

        Console.ReadLine();
    }
}

class MyThreadLimiter
{
    readonly SemaphoreSlim _semaphore = new SemaphoreSlim(24);

    public async Task Schedule()
    {
        await _semaphore.WaitAsync();

        Task.Run(() => Thread.Sleep(2000))
            .ContinueWith(t => _semaphore.Release());
    }
}

However replacing the Thread.Sleep with Task.Delay gives my expected results.

    public async Task Schedule()
    {
        await _semaphore.WaitAsync();

        Task.Delay(2000)
            .ContinueWith(t => _semaphore.Release());
    }

And using a Thread gives my expected results

    public async Task Schedule()
    {
        await _semaphore.WaitAsync();

        var thread = new Thread(() =>
        {
            Thread.Sleep(2000);
            _semaphore.Release();
        });
        thread.Start();
    }

How does Task.Run() work? Is it the case it is limited to the number of cores?

回答1:

Task.Run schedules the work to run in the thread pool. The thread pool is given wide latitude to schedule the work as best as it can in order to maximize throughput. It will create additional threads when it feels they will be helpful, and remove threads from the pool when it doesn't think it will be able to have enough work for them.

Creating more threads than your processor is able to run at the same time isn't going to be productive when you have CPU bound work. Adding more threads will just result in dramatically more context switches, increasing overhead, and reducing throughput.



回答2:

Yes for compute bound operations Task.Run() internally uses CLR's thread pool which will throttle the number of new threads to avoid CPU over-subscription. Initially it will run the number of threads that equals to the number of cpu cores concurrently. Then it continually optimises the number of threads using a hill-climbing algorithm based on factors like the number of requests thread pool receives and overall computer resources to either create more threads or fewer threads.

In fact, this is one of the main benefits of using pooled thread over raw thread e.g. (new Thread(() => {}).Start()) as it not only recycles threads but also optimises performance internally for you. As mentioned in the other answer, it's generally a bad idea to block pooled threads because it will "mislead" thread pool's optimisation, simiarly using many pooled thread to do very long-running computation can also lead to thread pool creating more threads and consequently increase the overheads of context switch and later destory extra threads in the pool.