"ждут Task.Yield()" и его альтернативы

Если мне нужно отложить выполнение кода до следующей итерации цикла сообщений потока пользовательского интерфейса, я мог бы сделать что-то вроде этого:

await Task.Factory.StartNew(
    () => {
        MessageBox.Show("Hello!");
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext());

Это будет похоже на await Task.Yield(); MessageBox.Show("Hello!");, кроме того, у меня будет возможность отменить задачу, если захочу.

В случае контекста синхронизации по умолчанию я мог бы аналогичным образом использовать await Task.Run для продолжения потока пула.

На самом деле мне нравится Task.Factory.StartNew и Task.Run больше, чем Task.Yield, потому что они оба явно определяют область действия кода продолжения.

Итак, в каких ситуациях await Task.Yield() действительно полезен?

Ответ 1

Рассмотрим случай, когда вы хотите, чтобы ваша задача async возвращала значение.

Существующий синхронный метод:

public int DoSomething()
{
    return SomeMethodThatReturnsAnInt();
}

Чтобы сделать async, добавьте ключевое слово async и измените тип возврата:

public async Task<int> DoSomething()

Чтобы использовать Task.Factory.StartNew(), измените однострочное тело метода на:

// start new task
var task = Task<int>.Factory.StartNew(
    () => {
        return SomeMethodThatReturnsAnInt();
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.FromCurrentSynchronizationContext() );

// await task, return control to calling method
await task;

// return task result
return task.Result;

против. добавив одну строку, если вы используете await Task.Yield()

// this returns control to the calling method
await Task.Yield();

// otherwise synchronous method scheduled for async execution by the 
// TaskScheduler of the calling thread
return SomeMethodThatReturnsAnInt();

Последний гораздо более сжатый, читаемый и действительно не сильно меняет существующий метод.

Ответ 2

Task.Yield() отлично подходит для "пробивания отверстия" в другой синхронной части метода async.

Лично я нашел его полезным в тех случаях, когда у меня есть метод самоотвержения async (тот, который управляет собственным соответствующим CancellationTokenSource и отменяет ранее созданный экземпляр при каждом последующем вызове), который можно вызвать несколько раз в течение очень короткого периода времени (т.е. обработчиков событий взаимозависимых элементов пользовательского интерфейса). В такой ситуации использование Task.Yield(), за которой следует проверка IsCancellationRequested, как только выгружается CancellationTokenSource, может помешать сделать потенциально дорогостоящую работу, результаты которой в конечном итоге будут отброшены.

Здесь показан пример, когда только последний вызов в очередь для SelfCancellingAsync позволяет выполнять дорогостоящую работу и запускаться до завершения.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskYieldExample
{
    class Program
    {
        private static CancellationTokenSource CancellationTokenSource;

        static void Main(string[] args)
        {
            SelfCancellingAsync();
            SelfCancellingAsync();
            SelfCancellingAsync();

            Console.ReadLine();
        }

        private static async void SelfCancellingAsync()
        {
            Console.WriteLine("SelfCancellingAsync starting.");

            var cts = new CancellationTokenSource();
            var oldCts = Interlocked.Exchange(ref CancellationTokenSource, cts);

            if (oldCts != null)
            {
                oldCts.Cancel();
            }

            // Allow quick cancellation.
            await Task.Yield();

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do the "meaty" work.
            Console.WriteLine("Performing intensive work.");

            var answer = await Task
                .Delay(TimeSpan.FromSeconds(1))
                .ContinueWith(_ => 42, TaskContinuationOptions.ExecuteSynchronously);

            if (cts.IsCancellationRequested)
            {
                return;
            }

            // Do something with the result.
            Console.WriteLine("SelfCancellingAsync completed. Answer: {0}.", answer);
        }
    }
}

Цель состоит в том, чтобы разрешить код, который выполняется синхронно на одном и том же SynchronizationContext сразу же после того, как не ожидаемый вызов метода async возвращает (когда он достигает своего первого await), чтобы изменить состояние, которое влияет на выполнение асинхронного метода. Это дросселируется так же, как и при использовании Task.Delay (я говорю о ненулевом периоде задержки здесь), но без фактической, потенциально заметной задержки, которая может быть нежелательной в некоторых ситуациях.

Ответ 3

Одна из ситуаций, когда Task.Yield() действительно полезна, - это когда вы await рекурсивно называетесь синхронно завершенными Task s. Поскольку csharps async/await "выпускает Zalgo" путем продолжения синхронного продолжения, когда он может, стек в сценарии полностью синхронной рекурсии может достаточно большой, чтобы ваш процесс умирал. Я думаю, что это также частично связано с тем, что tail-calls не может поддерживаться из-за косвенности Task. await Task.Yield() планирует продолжение, которое будет выполняться планировщиком, а не встроенным, что позволяет избежать роста в стеке, и эта проблема будет работать.

Кроме того, Task.Yield() можно использовать для обрезания синхронной части метода. Если вызывающему абоненту необходимо получить ваши методы Task, прежде чем ваш метод выполнит какое-либо действие, вы можете использовать Task.Yield() для принудительного возврата Task раньше, чем это было бы в противном случае. Например, в следующем сценарии локальных методов метод async может безопасно получить ссылку на свой собственный Task (при условии, что вы запускаете это на одном-concurrency SynchronizationContext, например, в winforms или через nitos AsyncContext.Run()):

using Nito.AsyncEx;
using System;
using System.Threading.Tasks;

class Program
{
    // Use a single-threaded SynchronizationContext similar to winforms/WPF
    static void Main(string[] args) => AsyncContext.Run(() => RunAsync());

    static async Task RunAsync()
    {
        Task<Task> task = null;
        task = getOwnTaskAsync();
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync()
        {
            // Cause this method to return and let the 「task」 local be assigned.
            await Task.Yield();
            return task;
        }
    }
}

выход:

3 == 3: True

Мне жаль, что я не могу придумать какие-либо сценарии реальной жизни, где возможность насильственного прерывания синхронной части метода async - лучший способ сделать что-то. Знать, что вы можете сделать трюк, как я только что показал, может быть полезным иногда, но он также более опасен. Часто вы можете передавать данные лучше, более читабельным и более потокобезопасным способом. Например, вы можете передать локальному методу ссылку на свой собственный Task, используя вместо TaskCompletionSource:

using System;
using System.Threading.Tasks;

class Program
{
    // Fully free-threaded! Works in more environments!
    static void Main(string[] args) => RunAsync().Wait();

    static async Task RunAsync()
    {
        var ownTaskSource = new TaskCompletionSource<Task>();
        var task = getOwnTaskAsync(ownTaskSource.Task);
        ownTaskSource.SetResult(task);
        var foundTask = await task;
        Console.WriteLine($"{task?.Id} == {foundTask?.Id}: {task == foundTask}");

        async Task<Task> getOwnTaskAsync(
            Task<Task> ownTaskTask)
        {
            // This might be clearer.
            return await ownTaskTask;
        }
    }
}

выход:

2 == 2: True

Ответ 4

Task.Yield не является альтернативой Task.Factory.StartNew или Task.Run. Они совершенно разные. Когда вы await Task.Yield, вы разрешаете выполнять другой код в текущем потоке без блокировки потока. Подумайте об этом, как ожидая Task.Delay, кроме Task.Yield ждет завершения задач, а не ждет определенного времени.

Примечание. Не используйте Task.Yield в потоке пользовательского интерфейса и предполагайте, что пользовательский интерфейс всегда будет реагировать. Это не всегда так.