Синхронный вызов vs await Task.Run() для CPU-bound операций .Net
Многие рекомендуют, как например здесь, использовать Task.Run() для Cpu-bound операций.
В статье по ссылке написано, что Task.Run() стоит использовать если:
- мы используем WinForms и хотим оставить наш UI поток отзывчивым
- мы хотим распараллелить задачи
- мы используем CPU-bound операции
Насчет CPU-bound операций у меня непонятка. Согласно рекоммендациям нужно писать так:
public async Task Foo()
{
await Task.Run(() => PerformLongCpuOperation());
}
public void PerformLongCpuOperation()
{
for (int i = 0; i < 10000; i++)
{
i = i * 32;
}
}`
Но что мы выиграем? Мы просто выносим операцию в фоновый поток. Да, вызывающий поток вернется в пул потоков, но у нас все так же будет один поток выполнять работу, плюс мы тратим ресурсы на переключение контекста. Не проще ли вызвать метод синхронно? Я не имею ввиду случаи с WinForms и WPF, где вызывающим потоком может быть UI поток.
Ответы (3 шт):
Эта рекомендация даётся для потоков UI, то есть именно для тех случаев WinForms и WPF, которые вы откинули.
Если работа в UI-потоке не предполагается, то проще и правда вызывать синхронно.
Если время выполнения метода меньше, чем время запуска нового потока ~30мс, и больше быть не может, смысла запускать его через Task мало. С другой стороны, сколько у вас пусков такого метода?
Если тяжелая однотипная операция повторяется в течение работы программы, то можно просто запустить долгоживущий поток, повесить там бесконечный цикл, ждущий получения команды на выполнение или/и аргументов и выполняющий нужную операцию немедленно (Producer-Consumer). В результате получится, что расходов на асинхронность практически нет, потому что не запускаем Task, а используем один долгоживущий, а обмен данными можно реализовать например через BlockingCollection, или/и коллбэк IProgress.
По поводу CPU-bound и I/O-bound еще написано здесь.
Конкретно для вашего примера можно выполняться синхронно.
Здесь нет красной кнопки "уничтожить всех человеков", или убойного шаблона программирования, который будет ответом на ваш вопрос и будет учитывать все случаи жизни. Пробуйсте несколько вариантов, профилируйте, замеряйте производительность. Отталкивайтесь от задачи и прямого ее решения, затем прямое решение постепенно превращайте в более элегантную, расширяемую и удобную реализацию.
Согласно рекоммендациям нужно писать так:
public async Task Foo()
{
await Task.Run(() => PerformLongCpuOperation());
}
Нет. Вовсе нет.
Во-первых, тут создаётся лишний конечный автомат. Правильнее писать так:
public Task Foo()
{
return Task.Run(() => PerformLongCpuOperation());
}
К Студии есть немало дополнений, которые указывают на эту ошибку и предлагают изменить код.
Во-вторых, такой метод-враппер бессмысленен и не нужен.
Task.Run Etiquette - серия статей от Stephen Cleary.
Кратко: если метод PerformLongCpuOperation по сути своей синхронный - то он должен существовать сам по себе, без лишних обёрток. Когда нужно запустить его в отдельном потоке, Task.Run пишется по месту.
Если он по сути своей асинхронный, то это совсем другое дело.