Использование Task.WhenAll для нескольких асинхронных и поддельных асинхронных методов - PullRequest
0 голосов
/ 18 декабря 2018

Мой коллега провел рефакторинг методов нашего контроллера, чтобы все наши операции ввода-вывода, включая синхронные, инкапсулировались в отдельные задачи, а затем все эти задачи выполнялись параллельно через 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);
}

Ответы [ 4 ]

0 голосов
/ 18 декабря 2018

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

Сравните:

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.Но не просто приказывайте другим потокам делать что-то, не делая ничего самостоятельно.

0 голосов
/ 18 декабря 2018

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

Оригинал:

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. Держись за основной поток.Если какая-то другая задача нуждается в потоке, ей придется раскрутить свою собственную.

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

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

0 голосов
/ 18 декабря 2018

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

0 голосов
/ 18 декабря 2018

В целом, с вашим кодом все в порядке - он будет потреблять больше потоков и немного больше ресурсов ЦП, чем оригинал, но если ваш сайт не будет сильно загружен, это вряд ли существенно повлияет на общую производительность.Очевидно, вам нужно измерить его самостоятельно для вашей конкретной нагрузки (включая некоторую нагрузку на уровне нагрузки в 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...