#c_sharp #async_await #async #async_programming
Недавно прочитал статью на хабре (upd: из комментов понял, что нужно прицепить цитату, по которой далее вопрос) Как только код доходит до метода Task.Run(), достаётся другой поток из пула потоков и в нём исполняется код, который мы передали в Task.Run(). Старый поток, как и положено приличному потоку, возвращается в пул и ждёт, когда его снова позовут делать работу. Новый поток выполняет переданный код, доходит до синхронной операции, синхронно выполняет её (ждёт пока операция не будет выполнена) и идёт дальше по коду. Иными словами, операция так и осталась синхронной: мы, как и раньше, используем поток во время выполнения синхронной операции. Единственное отличие — мы потратили время на переключение контекста при вызове Task.Run() и при возврате в ExecuteOperation(). Всё стало немножечко хуже. Один из вопросов, который там рассматривается: вызов Task.Run - это антипаттерн, и нужен он только для отзывчивости GUI. Вопрос именно про Task.Run(() => _anyWork()), где _anyWork() содержит синхронный код. То, что написано в статье, звучит достаточно логично, если делать так: await DoWork(); ... Task DoWork() => Task.Run(_work); Да, в таком случае создается лишняя нагрузка на пул потоков. Но, ведь если делать так: var task1 = DoWork1(); var task2 = DoWork2(); var task3 = DoWork3(); await Task.WhenAll(task1, task2, task3); Task.Run сразу превращается в нормальный код, ведь так? Поток, который будет выполнять этот код, создаст три других потока (upd: оговорился: инициирует добавление работы в очередь, которая будет запущена в ThreadPool), которые параллельно будут выполнять свою работу параллельно. Дальше, когда он встретит await - он вернет управление (в итоге, скорее всего, вернется в пул). Исправьте, пожалуйста, если не так. Если это так - возникает вопрос: где проходит эта грань, между плохой реализацией, и нормальной? В небиблиотечном коде понятно - если вызывается метод, а ожидание где-то дальше - то можно делать Task.Run. В библиотечном же - с одной стороны, мы можем распараллелить работу своих методов, если клиент будет ожидать их после вызова. С другой - мы можем зря увеличить нагрузку, если клиент будет ожидать результат сразу при вызове. Знать точно, как будет вызывать методы клиент - мы не можем, можем только дать рекомендации в документации. Возможно есть какие-то официальные рекомендации MS? На msdn я нашел только сухое описание работы методов.
Ответы
Ответ 1
Один из вопросов, который там рассматривается: вызов Task.Run - это антипаттерн, и нужен он только для отзывчивости GUI. Task.Run Method Ставит в очередь заданную работу для запуска в ThreadPool и возвращает задачу или дескриптор Task для этой работы. То есть Task.Run - это способ выполнить какую-то работу в пуле потоков с возможностью ожидания результата асинхронно. Когда это может понадобится? Возможные примеры использования: 1) Запуск IO/CPU нагрузки пуле потоков, чтобы не грузить основной UI поток (пример) 2) Запуск асинхронного кода синхронно из UI потока. Такое может потребоваться, когда у вас большое приложение и вы постепенно переходите на асинхронные вызовы вместо синхронных, но не везде пока можете вызывать ваш новый API асинхронно, пр этом вам надо избегать дедлоков в UI потоке, например Task.Run(()=>CallSmthgAsync().GetAwaiter().GetResult()).GetAwaiter().GetResult(); - это не оч красивый код и он должен быть исправлен, но не всегода можно внедрить асинхронные вызовы за один заход, потому я такое сам делал и видел иногда. Когда это НЕ надо: когда вас не заботит факт блокировки текущего потока какой то синхронной работой. В прмере в вопросе задача и так уже запущена в пуле потоков, потому переключаться в другой поток и там выполнять что то синхронное смысла не имеет. Но если бы та же работа была запущена из UI потока, то без Task.Run все UI приложение бы встало колом. Что касается библиотечного кода. Стройте ваш API и его реализацию исходя из требований и вариантов использования. Если у вас много I/O операций (работа с файлами, с сетью, с БД), то это по сути самой собой намекает на необходимость асинхронного API. Если вас смущает выбор между синхронным API и асинхронным, то реализуйте оба, пусть клиент выбирает,что ему нужно. Отсюда вывод: самое главное, чтобы не наломать дров, всегда знайте что вы делаете и зачем вы это делаете. Переключайте контекст тогда, когда у вас есть причина это делать. Если каждая строка вашего кода будет обоснована и иметь причину, почему она именно так написана, то проблем с выбором хороший код/ плохой код у вас будет гораздо меньше.Ответ 2
Код метода *Async выполняется в текущем потоке до первого await. И текущий поток будет захвачен. Например, HttpClient до начала асинхронного запроса синхронно запрашивает dns, что может быть долгой операцией. И вы можете решить, что вас больше беспокоит - ожидание текущего потока или накладные расходы от Task.Run() (которые минимальны по факту, разве что раздувают код)Ответ 3
использовать Task: если внутри нужно использовать асинхронную операцию: внутри их несколько синхронный код и асинхронная операция вот интересная статья о using(IDisposable) и потенциальном баге если ты делаешь свою собственную асинхронную операцию (например превращаешь событие в Task) - часто используют TaskCompletionSource второе похоже на I/O-bound операцию - не уверен, писать ли отдельным пунктом
Комментариев нет:
Отправить комментарий