Async-await использует потоки, и если случится так, что все потоки пула потоков будут заняты в то время, то создаются новые потоки.Поэтому говорить, что async-await не создает потоков, не совсем точно.Разница в том, как эти потоки обычно используются.В классическом многопоточном сценарии вы создаете новые потоки с намерением поддерживать их в течение некоторого времени.Вы не запускаете новый поток для выполнения рабочей нагрузки в 1 миллисекунду.Обычно вы выполняете вычисления с интенсивным использованием ЦП, либо читаете с диска большие файлы, либо вызываете какой-либо веб-метод, для ответа на который может потребоваться некоторое время.В некоторых из этих примеров потоки фактически не выполняют большую работу, они просто ждут большую часть времени, чтобы получить данные с диска или сетевых драйверов.И пока они ждут, они потребляют довольно большой кусок памяти, около 1 МБ каждого потока, просто для собственного существования.
Войдите в мир асинхронного ожидания, где очень малопотоки могут тратить ресурсы, будучи простаивающими.Это потоки общего пула потоков, готовые отвечать на события от драйверов, от часов компьютера или откуда-либо еще.Обычно каждая отдельная рабочая нагрузка потока пула потоков довольно мала.Это может быть одна миллисекунда или даже меньше.Инфраструктура .NET впечатляет своей способностью обрабатывать огромное количество запланированных задач:
var stopwatch = Stopwatch.StartNew();
var tasks = new List<Task>();
int sum = 0;
foreach (var i in Enumerable.Range(0, 100_000))
{
tasks.Add(Task.Run(async () =>
{
await Task.Delay(i % 1000);
Interlocked.Increment(ref sum);
}));
}
Console.WriteLine($"Waiting, Elapsed: {stopwatch.ElapsedMilliseconds} msec");
await Task.WhenAll(tasks);
Console.WriteLine($"Sum: {sum}, Elapsed: {stopwatch.ElapsedMilliseconds} msec");
Ожидание, истекло: 296 мсек
Сумма: 100000, истекло: 1898 мсек
Это с .NET Framework 4.8..NET Core 3.0 работает еще лучше.
Чем больше вы работаете с задачами и async-await, тем больше вы доверяете инфраструктуре и даете ей все более и более детальную работу.Например, чтение строк из текстового файла.Вы бы сошли с ума, чтобы начать поток, просто чтобы прочитать одну строку из файла, но с задачами и async-await это совсем не сумасшествие: StreamReader.ReadLineAsync
.