Страницы

Поиск по вопросам

понедельник, 25 ноября 2019 г.

Что такое Task.Yield()?


Я не понимаю что это, как работает и в каких случаях используется. Может кто-нибудь по-русски объяснить?
    


Ответы

Ответ 1



Этот метод возвращает специальное значение, предназначенное для передачи оператору await, и в отрыве от этого оператора не имеющее смысла. Конструкция же await Task.Yield() делает довольно простую вещь — прерывает текущий метод и сразу же планирует его продолжение в текущем контексте синхронизации. Используется же эта конструкция для разных целей. Во-первых, эта конструкция может быть использована для немедленного возврата управлени вызывающему коду. Например, при вызове из обработчика события событие будет считаться обработанным: protected override async void OnClosing(CancelEventArgs e) { e.Cancel = true; await Task.Yield(); // (какая-то логика) } Во-вторых, эта конструкция используется для очистки синхронного контекста вызова. Например, так можно "закрыть" текущую транзакцию (ambient transaction): using (var ts = new TransactionScope()) { // ... Foo(); // ... ts.Complete(); } async void Foo() { // ... тут мы находимся в контексте транзакции if (Transaction.Current != null) await Task.Yield(); // ... а тут его уже нет! } В-третьих, эта конструкция может очистить стек вызовов. Это может быть полезным если программа падает с переполнением стека при обработке кучи вложенных продолжений. Например, рассмотрим упрощенную реализацию AsyncLock: class AsyncLock { private Task unlockedTask = Task.CompletedTask; public async Task Lock() { var tcs = new TaskCompletionSource(); await Interlocked.Exchange(ref unlockedTask, tcs.Task); return () => tcs.SetResult(null); } } Здесь поступающие запросы на получение блокировки выстраиваются в неявную очередь на продолжениях. Казалось бы, что может пойти не так? private static async Task Foo() { var _lock = new AsyncLock(); var unlock = await _lock.Lock(); for (var i = 0; i < 100000; i++) Bar(_lock); unlock(); } private static async void Bar(AsyncLock _lock) { var unlock = await _lock.Lock(); // do something sync unlock(); } Здесь продолжение метода Bar вызывается в тот момент, когда другой метод Bar выполняе вызов unlock(). Получается косвенная рекурсия между методом Bar и делегатом unlock, которая быстро сжирает стек и ведет к его переполнению. Добавление же вызова Task.Yield() перенесет исполнение в "чистый" фрейм стека, ошибка исчезнет: class AsyncLock { private Task unlockedTask = Task.CompletedTask; public async Task Lock() { var tcs = new TaskCompletionSource(); var prevTask = Interlocked.Exchange(ref unlockedTask, tcs.Task); if (!prevTask.IsCompleted) { await prevTask; await Task.Yield(); } return () => tcs.SetResult(null); } } Кстати, альтернативный способ починить код выше — использование флага RunContinuationsAsynchronously: class AsyncLock { private Task unlockedTask = Task.CompletedTask; public async Task Lock() { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await Interlocked.Exchange(ref unlockedTask, tcs.Task); return () => tcs.SetResult(null); } } В-четвертых, при использовании в UI-потоке эта конструкция позволяет обработать накопившиеся события ввода-вывода, что полезно при длительных обновлениях интерфейса. Например, при добавлении миллиона строк в таблицу программа не будет реагироват на действия пользователя, пока все строки не будут добавлены. Но если, к примеру, после добавления каждой тысячи строк вставлять вызов await Task.Yield() - программа сможет обрабатывать действия пользователя и не будет выглядеть зависшей. В WinForms для тех же целей можно было использовать метод Application.DoEvents( - но его избыточное использование приводило к переполнению стека. await Task.Yield() - это универсальный способ, который можно использовать как в WinForms, так и в WPF.

Ответ 2



Я думаю, что здесь никто не ответил на вопрос, зачем нужна Task.Yield. Она нужна когда задача (Task) использует бесконечный цикл (вообще любая продолжительная синхронная работа) и может удерживать поток из пула только для себя и не давать други задачам использовать этот поток. Task.Yield переотправляет задачу в очередь пула потоков и другие задачи, которые ожидали выполнения смогут использовать данный удерживаемый поток. Пример: CancellationTokenSource cts; void Start() { cts = new CancellationTokenSource(); // Запускаем асинхронную операцию var task = Task.Run(() => SomeWork(cts.Token), cts.Token); // Ждем окончания // После окончания операции обрабатываем результат/отмену/исключения } async Task SomeWork(CancellationToken cancellationToken) { int result = 0; bool loopAgain = true; while (loopAgain) { // Что-то делаем ... loopAgain = /* проверка на окончание цикла && */ cancellationToken.IsCancellationRequested; if (loopAgain) { // переотправляет задачу в очередь пула потоков чтобы другие задачи, которые ожидали выполнения смогли использовать данный поток await Task.Yield(); } } cancellationToken.ThrowIfCancellationRequested(); return result; } void Cancel() { // Запрашиваем отмену операции cts.Cancel(); }

Комментариев нет:

Отправить комментарий