Использование Task.WhenAll для нескольких асинхронных и поддельных асинхронных методов

Мой коллега реорганизовал наши методы контроллера, чтобы все наши операции ввода-вывода, включая синхронные, были инкапсулированы в отдельные задачи, а затем все эти задачи выполнялись параллельно через Task.WhenAll. Я могу определенно понять идею: мы используем больше потоков, но все наши операции ввода-вывода (а у нас может быть довольно много) выполняются со скоростью самой медленной, но я все еще не уверен, правильный ли это путь. . Это правильный подход или я что-то упускаю? Будет ли заметна стоимость использования большего количества потоков в типичном веб-приложении ASP.Net? Вот пример кода

public async Task<ActionResult> Foo() {
    var dataATask = _dataARepository.GetDataAsync();
    var dataBTask = Task.Run(_dataBRepository.GetData());
    await Task.WhenAll(dataATask, dataBTask);
    var viewModel = new ViewModel(dataATask.Result, dataBTask.Result);
    return View(viewModel);
}

почему синхронизация _dataBRepository.GetData()?

zaitsman 18.12.2018 01:39

Потому что так получилось, и его рефакторинг выходит за рамки задачи.

AndreySarafanov 18.12.2018 01:40

вот где на самом деле ваша проблема. async эффективен только при полном подключении к операции ввода-вывода. Обернуть метод синхронизации в Task.Run будет еще хуже, чем просто вызвать его в строке, потому что вы все еще блокируете поток, но теперь вы также добавляете накладные расходы на конечный автомат.

zaitsman 18.12.2018 01:41

Возможно, мне не хватает понимания. Я понимаю, что не смогу повторно использовать поток для обслуживания другого запроса во время выполнения операции Task.Run, но будет ли запуск нескольких синхронных операций с await Task.WhenAll увеличит скорость, с которой возвращается один запрос?

AndreySarafanov 18.12.2018 01:46

@Jacob Task.Run всегда планирует работу с пулом потоков. Статья MSDN, о котором вы говорите, не о Task.Run ...

Alexei Levenkov 18.12.2018 01:47

Дело в том, что Task.Run заблокирует поток Другая, я это понимаю. Дело в том, чтобы запрос был быстрее обслужен. Я пожертвую другими потоками, заблокировав их.

AndreySarafanov 18.12.2018 01:50
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
1 299
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

В целом ваш код в порядке - он будет потреблять больше потоков и немного больше ЦП, чем исходный, но если ваш сайт не будет сильно загружен, это вряд ли существенно повлияет на общую производительность. Очевидно, вам нужно измерить его самостоятельно для вашей конкретной нагрузки (включая нагрузку с уровнем стресса, превышающую 5-10-кратный регулярный трафик).

Оборачивание синхронного метода в Task.Run - не лучшая практика (см. Должен ли я предоставлять асинхронные оболочки для синхронных методов?). Это может сработать для вас, если торговля дополнительными потоками для такого поведения приемлема для вашего случая.

Если у вас осталась только одна синхронная операция, вы можете вместо этого сохранить ее синхронной и дождаться остальных в конце синхронного шага, сохраняя этот дополнительный поток:

var dataATask = _dataARepository.GetDataAsync();
var dataBTaskResult = _dataBRepository.GetData();
await Task.WhenAll(dataATask); // or just await dataATask if you have only one.
var viewModel = new ViewModel(dataATask.Result, dataBTaskResult);

Я понимаю это так же, но мне бы хотелось иметь некоторое представление о количестве потоков в пуле потоков и некоторое теоретическое представление о нагрузке, когда это больше не ускоряет работу сайта. Я понимаю, что он должен быть довольно низким, иначе это не считалось бы такой плохой практикой. Я, может, провожу тестирование, но это может быть просто потеря времени для проверки бессмысленной идеи (что, скорее всего, так и есть).

AndreySarafanov 18.12.2018 02:14

@AndreySarafanov количество потоков в пуле потоков - много (stackoverflow.com/a/6000891/477420), но если вы используете большинство из них (т.е. вы потребляете 10 потоков на запрос для ваших оболочек async-sync и есть достаточная нагрузка), потоков будет недостаточно для Продолжать исполнение запросов. Вы должны иметь возможность вычислить количество потоков, которые ваш сайт будет использовать при нормальной / пиковой нагрузке (не забывайте, что каждая операция требует времени), и решить, достаточно ли потоков у вас.

Alexei Levenkov 18.12.2018 02:32

Рассмотрим эти два подхода.

Оригинал:

public async Task<ActionResult> Foo() 
{
    var dataATask = _dataARepository.GetDataAsync();
    var dataBTask = Task.Run(_dataBRepository.GetData());
    await Task.WhenAll(dataATask, dataBTask);
    var viewModel = new ViewModel(dataATask.Result, dataBTask.Result);
    return View(viewModel);
}

^ Эта версия создаст новый поток для вызова _dataBRepository.GetData(). Дополнительный поток будет заблокирован до завершения вызова. Ожидая завершения дополнительного потока, основной поток вернет управление конвейеру ASP.NET, где он может обработать запрос другого пользователя.

Разные:

public async Task<ActionResult> Foo() 
{
    var dataATask = _dataARepository.GetDataAsync();
    var dataBResult = _dataBRepository.GetData();
    await dataATask;
    var viewModel = new ViewModel(dataATask.Result, dataBResult);
    return View(viewModel);
}

^ Эта версия не создает отдельную ветку для dataBRepository.GetData(). Но он блокирует основной поток.

Итак, ваш выбор:

  1. Разверните другой поток, чтобы вы могли передать основной поток какой-либо другой задаче.
  2. Держитесь за основную нить. Если какой-то другой задаче нужен поток, он должен будет развернуть свой собственный.

В обоих случаях вы используете по одному потоку за раз (по большей части). В обоих случаях транзакция завершится за время, необходимое для более медленной из двух внутренних транзакций. Но исходный вариант запускает новый поток и дает текущий. Это похоже на лишнюю работу, в которой нет необходимости.

С другой стороны, если для действия требуются две или более синхронных серверных транзакции, исходный вариант завершится быстрее, поскольку они могут выполняться параллельно. При условии, что они это поддерживают. Если внутренние транзакции являются транзакциями базы данных и используют одну и ту же базу данных, они могут блокировать друг друга, и в этом случае вы все равно не увидите никакой выгоды.

Я намеренно упростил пример, я говорю о нескольких задачах синхронизации, надо было сказать это яснее. Часто это транзакции с базами данных, но из разных, достаточно разных, чтобы не блокировать друг друга. И я прекрасно понимаю, что происходит за кулисами, мой вопрос был в основном о практической полезности такого подхода. накладные расходы потока могут просто затмить повышение скорости.

AndreySarafanov 18.12.2018 10:54

И, очевидно, нам следует сделать больше асинхронного ввода-вывода, но это довольно большая работа в будущем.

AndreySarafanov 18.12.2018 10:55

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

John Wu 18.12.2018 11:08

Не думаю, что заслуживаю сарказма. Но ваш ответ состоит в том, что я полагаю, это неразумно в большинстве реальных условий. Спасибо за это.

AndreySarafanov 18.12.2018 11:14

Нет, это не так, и я не был саркастичен. Дело в том, что есть конкурирующие опасения. Вы получите максимальное количество транзакций / пользователей без увеличения количества потоков, поэтому, если у вас мало оборудования, это может быть подходящим вариантом. Но если у вас мощный центр обработки данных и вы просто хотите, чтобы транзакции завершались быстрее, возможно, вам нужно развернуть новые потоки для параллельной работы.

John Wu 19.12.2018 02:53

Извините, я неправильно понял. Спасибо за ответ!

AndreySarafanov 19.12.2018 03:13

Помимо Алексей Левенков упомянул об открытии синхронных оберток для асинхронных методов, использование Task.Run в приложении ASP.NET принесет больше вреда, чем пользы. Каждый Task.Run вызывает 2 планирования пула потоков и переключение контекста без каких-либо преимуществ.

Я считаю, что в этом есть какая-то польза, и я достаточно ясно ее описал. Хотя вред может быть намного больше, да.

AndreySarafanov 18.12.2018 11:05

@AndreySarafanov, пользы нет. В зависимости от варианта использования всегда есть лучшие способы сделать это.

Paulo Morgado 18.12.2018 14:59

Обычно бесполезно запускать другой поток для выполнения каких-либо синхронных действий, если все, что вы делаете, это дожидаетесь завершения этого потока.

Сравнивать:

async Task<int> ProcessAsync()
{
    var task = Task.Run(() => DoSomeHeavyCalculations());
    int calculationResult = await task;
    return calculationResult;
}

с участием

int ProcessAsync()
{
    return DoSomeHeavyCalculations();
}

Помимо того, что асинхронная функция использует больше интеллектуальных ресурсов, она ограничивает повторное использование функции: ее могут использовать только вызывающие асинхронные вызовы. Позвольте вашему абоненту решить, хочет ли он звонить вам асинхронно или нет. Если ему также нечего делать, кроме как ждать, он может позволить своему абоненту решить и т. д.

Кстати, это именно то, что делает GetData: он не заставляет вызывающих абонентов, таких как вы, быть асинхронными, он дает вам свободу называть его синхронным или использовать Task.Run, чтобы называть его асинхронным.

Однако, если у вашей функции есть чем заняться во время тяжелых вычислений, это может быть полезно:

async Task<int> ProcessAsync()
{
    var task = Task.Run(() => DoSomeHeavyCalculations());

    // while the calculations are being performed, do something else:
    DoSomethingElse();

    return await task;
}

В вашем примере: если есть только один «тяжелый расчет», сделайте это самостоятельно. Если есть несколько тяжелых расчетов, вы можете рассмотреть возможность использования Task.Run. Но не просто приказывайте другим потокам что-то делать, ничего не делая сами.

Другие вопросы по теме