Мой коллега реорганизовал наши методы контроллера, чтобы все наши операции ввода-вывода, включая синхронные, были инкапсулированы в отдельные задачи, а затем все эти задачи выполнялись параллельно через 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);
}
Потому что так получилось, и его рефакторинг выходит за рамки задачи.
вот где на самом деле ваша проблема. async эффективен только при полном подключении к операции ввода-вывода. Обернуть метод синхронизации в Task.Run будет еще хуже, чем просто вызвать его в строке, потому что вы все еще блокируете поток, но теперь вы также добавляете накладные расходы на конечный автомат.
Возможно, мне не хватает понимания. Я понимаю, что не смогу повторно использовать поток для обслуживания другого запроса во время выполнения операции Task.Run, но будет ли запуск нескольких синхронных операций с await Task.WhenAll увеличит скорость, с которой возвращается один запрос?
@Jacob Task.Run всегда планирует работу с пулом потоков. Статья MSDN, о котором вы говорите, не о Task.Run ...
Дело в том, что Task.Run заблокирует поток Другая, я это понимаю. Дело в том, чтобы запрос был быстрее обслужен. Я пожертвую другими потоками, заблокировав их.





В целом ваш код в порядке - он будет потреблять больше потоков и немного больше ЦП, чем исходный, но если ваш сайт не будет сильно загружен, это вряд ли существенно повлияет на общую производительность. Очевидно, вам нужно измерить его самостоятельно для вашей конкретной нагрузки (включая нагрузку с уровнем стресса, превышающую 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 количество потоков в пуле потоков - много (stackoverflow.com/a/6000891/477420), но если вы используете большинство из них (т.е. вы потребляете 10 потоков на запрос для ваших оболочек async-sync и есть достаточная нагрузка), потоков будет недостаточно для Продолжать исполнение запросов. Вы должны иметь возможность вычислить количество потоков, которые ваш сайт будет использовать при нормальной / пиковой нагрузке (не забывайте, что каждая операция требует времени), и решить, достаточно ли потоков у вас.
Рассмотрим эти два подхода.
Оригинал:
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(). Но он блокирует основной поток.
Итак, ваш выбор:
В обоих случаях вы используете по одному потоку за раз (по большей части). В обоих случаях транзакция завершится за время, необходимое для более медленной из двух внутренних транзакций. Но исходный вариант запускает новый поток и дает текущий. Это похоже на лишнюю работу, в которой нет необходимости.
С другой стороны, если для действия требуются две или более синхронных серверных транзакции, исходный вариант завершится быстрее, поскольку они могут выполняться параллельно. При условии, что они это поддерживают. Если внутренние транзакции являются транзакциями базы данных и используют одну и ту же базу данных, они могут блокировать друг друга, и в этом случае вы все равно не увидите никакой выгоды.
Я намеренно упростил пример, я говорю о нескольких задачах синхронизации, надо было сказать это яснее. Часто это транзакции с базами данных, но из разных, достаточно разных, чтобы не блокировать друг друга. И я прекрасно понимаю, что происходит за кулисами, мой вопрос был в основном о практической полезности такого подхода. накладные расходы потока могут просто затмить повышение скорости.
И, очевидно, нам следует сделать больше асинхронного ввода-вывода, но это довольно большая работа в будущем.
Ну, вы можете иметь столько потоков, сколько хотите ... путем добавления серверов. Я так понимаю, это балансировка нагрузки?
Не думаю, что заслуживаю сарказма. Но ваш ответ состоит в том, что я полагаю, это неразумно в большинстве реальных условий. Спасибо за это.
Нет, это не так, и я не был саркастичен. Дело в том, что есть конкурирующие опасения. Вы получите максимальное количество транзакций / пользователей без увеличения количества потоков, поэтому, если у вас мало оборудования, это может быть подходящим вариантом. Но если у вас мощный центр обработки данных и вы просто хотите, чтобы транзакции завершались быстрее, возможно, вам нужно развернуть новые потоки для параллельной работы.
Извините, я неправильно понял. Спасибо за ответ!
Помимо Алексей Левенков упомянул об открытии синхронных оберток для асинхронных методов, использование Task.Run в приложении ASP.NET принесет больше вреда, чем пользы. Каждый Task.Run вызывает 2 планирования пула потоков и переключение контекста без каких-либо преимуществ.
Я считаю, что в этом есть какая-то польза, и я достаточно ясно ее описал. Хотя вред может быть намного больше, да.
@AndreySarafanov, пользы нет. В зависимости от варианта использования всегда есть лучшие способы сделать это.
Обычно бесполезно запускать другой поток для выполнения каких-либо синхронных действий, если все, что вы делаете, это дожидаетесь завершения этого потока.
Сравнивать:
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. Но не просто приказывайте другим потокам что-то делать, ничего не делая сами.
почему синхронизация
_dataBRepository.GetData()?