Должно ли использоваться меньшее количество потоков, если я использую async?

Я понимаю, что если я использую async, поток делает веб-запрос и движется дальше. Когда ответ возвращается, другой поток поднимает его оттуда. Таким образом, существует меньшее количество связанных нитей, сидящих без дела. Разве это не означает, что максимальное количество живых потоков снизится? Но в приведенном ниже примере код, который не использует async, заканчивается использованием меньшего количества потоков. Может кто-нибудь объяснить, почему?

Код без async (использует меньшие потоки):

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;

namespace NoAsync
{
    internal class Program
    {
        private const int totalCalls = 100;

        private static void Main(string[] args)
        {
            for (int i = 1; i <= totalCalls; i++)
            {
                ThreadPool.QueueUserWorkItem(GoogleSearch, i);
            }

            Thread.Sleep(100000);
        }

        private static void GoogleSearch(object searchTerm)
        {
            Thread.CurrentThread.IsBackground = false;
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse) wr.GetResponse();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = reader.ReadToEnd();
            //Console.WriteLine(responseFromServer); // Display the content.
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
        }
    }
}

Код с async (использует больше потоков)

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace AsyncAwait
{
    internal class Program
    {
        private const int totalCalls = 100;
        private static DateTime start = System.DateTime.Now;

    private static void Main(string[] args)
    {
        var tasks = new List<Task>();

        for (int i = 1; i <= totalCalls; i++)
        {
            var searchTerm = i;
            var t = GoogleSearch(searchTerm);
            tasks.Add(t);
        }

        Task.WaitAll(tasks.ToArray());
        Console.WriteLine("Hit Enter to exit");
        Console.ReadLine();
    }

    private static async Task GoogleSearch(object searchTerm)
    {
        Thread.CurrentThread.IsBackground = false;
        string url = @"https://www.google.com/search?q=" + searchTerm;
        Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
        using (var client = new HttpClient())
        {
            using (HttpResponseMessage response = await client.GetAsync(url))
            {
                HttpContent content = response.Content;
                content.Dispose();
                Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
                Console.WriteLine("TimeSpan consumed {0}", System.DateTime.Now.Subtract(start));
            }
        }
    }
}

}

Я понимаю, что мои результаты включают неуправляемые потоки. Но не должно ли общее число потоков быть ниже?

Обновление: Я обновил асинхронный вызов с кодом, предоставленным Noseratio

Ответ 1

ThreadPool фактически поддерживает два под-пула: один для рабочих потоков и другой для потоков IOCP. Когда доступен результат GetAsync, случайный поток IOCP (порт завершения ввода/вывода) распределяется из пула для обработки завершения асинхронного HTTP-запроса. То, где выполняется код после await. У вас есть контроль над размером каждого под-пула с ThreadPool.SetMinThreads/SetMaxThreads, подробнее об этом ниже.

Как и ваш, не-асинхронный код вряд ли можно сравнить с асинхронной версией. Для более справедливого сравнения вы должны придерживаться WebRequest для обоих случаев, например:

Non-Асинхронный:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace NoAsync
{
    internal class Program
    {
        private const int totalCalls = 100;

        private static void Main(string[] args)
        {
            int maxWorkers, maxIOCPs;
            ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
            int minWorkers, minIOCPs;
            ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
            Console.WriteLine(new { maxWorkers, maxIOCPs, minWorkers, minIOCPs });

            ThreadPool.SetMinThreads(100, 100);

            var tasks = new List<Task>();

            for (int i = 1; i <= totalCalls; i++)
                tasks.Add(Task.Run(() => GoogleSearch(i)));

            Task.WaitAll(tasks.ToArray());
        }

        private static void GoogleSearch(object searchTerm)
        {
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse)wr.GetResponse();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = reader.ReadToEnd();
            //Console.WriteLine(responseFromServer); // Display the content.
            reader.Close();
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
        }
    }
}

Асинхронный

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Async
{
    internal class Program
    {
        private const int totalCalls = 100;

        private static void Main(string[] args)
        {
            int maxWorkers, maxIOCPs;
            ThreadPool.GetMaxThreads(out maxWorkers, out maxIOCPs);
            int minWorkers, minIOCPs;
            ThreadPool.GetMinThreads(out minWorkers, out minIOCPs);
            Console.WriteLine(new { maxWorkers, maxIOCPs, minWorkers, minIOCPs });

            ThreadPool.SetMinThreads(100, 100);

            var tasks = new List<Task>();

            for (int i = 1; i <= totalCalls; i++)
                tasks.Add(GoogleSearch(i));

            Task.WaitAll(tasks.ToArray());
        }

        private static async Task GoogleSearch(object searchTerm)
        {
            string url = @"https://www.google.com/search?q=" + searchTerm;
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
            WebRequest wr = WebRequest.Create(url);
            var httpWebResponse = (HttpWebResponse) await wr.GetResponseAsync();
            var reader = new StreamReader(httpWebResponse.GetResponseStream());
            string responseFromServer = await reader.ReadToEndAsync();
            //Console.WriteLine(responseFromServer); // Display the content.
            reader.Close();
            httpWebResponse.Close();
            Console.WriteLine("Total number of threads in use={0}", Process.GetCurrentProcess().Threads.Count);
        }
    }
}

По умолчанию для пула потоков в моей системе я вижу следующие цифры:

{ maxWorkers = 32767, maxIOCPs = 1000, minWorkers = 4, minIOCPs = 4 }

Пул потоков ленив при выращивании потоков. Новое создание потока может быть отложено до 500 мс (подробнее см. Joe Duffy "CLR thread pool injection, stuttering problems" ).

Для учета этого поведения используйте ThreadPool.SetMinThreads. С SetMinThreads(100, 100) я вижу ~ 111 потоков на пике для версии синхронизации и ~ 20 потоков в пике для асинхронной версии (релиз сборки, работающий без отладчика). Это довольно показательная разница, от имени асинхронной версии.