Если в это верить, то тысяча задач — тысяча потоков. И начинается: задачи боятся создавать, запросы к API заворачивают в Task.Run, чтобы точно ушли в фон, а любой WhenAll кажется миной.
Я проверил. Запустил разом 10 000 задач:
var tasks = Enumerable.Range(0, 10_000)
.Select(_ => Task.Delay(1000))
.ToArray();
await Task.WhenAll(tasks);Всё закончилось за 1029 мс. Потоков в процессе в это время было 12, из них в пуле два 🥰
Task — квитанция из химчистки. Тебе дали бумажку: результат будет, приходи потом. Кто стирает и стирает ли вообще прямо сейчас, в квитанции не написано. С Task.Delay никто не стирает: есть таймер, который через секунду скажет "готово". С запросом к базе то же самое, ответа ждёт сетевая карта и ОС, а не твой поток.
Поток — работник. Task — обещание результата. Иногда за обещанием стоит работник (Task.Run), а иногда его там нет совсем.
Так что вот это ничего не даёт, кроме лишнего прыжка в пул:
await Task.Run(() => _http.GetStringAsync(url));
HTTP-запрос и без него не держит поток, пока ждёт ответ.
Поищи у себя в проекте Task.Run вокруг HttpClient. Я подожду 🫡
Я сделал тест по асинхронности и многопоточности у себя на dotnetdad: 10 вопросов, минут на пять. В конце разбор ответов и уроки ровно по тем местам, где ошибся 🌟