Страницы

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

вторник, 2 октября 2018 г.

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

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


Ответ

Этот метод возвращает специальное значение, предназначенное для передачи оператору 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

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

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