Отсутствие не захвата Task.Yield заставляет меня использовать Task.Run, зачем следовать этому?

Извините заранее, если этот вопрос основан на мнениях. Отсутствие Task.Yield версии, которая не захватила бы контекст выполнения, уже обсуждалась здесь. По-видимому, эта функция присутствовала в некоторой форме в ранних версиях Async CTP, но была удалена, потому что ее можно было легко использовать.

IMO, такая функция может быть так же легко использована как Task.Run. Вот что я имею в виду. Представьте себе ожидаемый API SwitchContext.Yield, который планирует продолжение в ThreadPool, поэтому выполнение всегда будет продолжаться в потоке, отличном от вызывающего потока. Я мог бы использовать его в следующем коде, который запускает некоторую работу, связанную с ЦП, из потока пользовательского интерфейса. Я бы счел это удобным способом продолжения работы, связанной с процессором, в потоке пула:

class Worker
{
    static void Log(string format, params object[] args)
    {
        Debug.WriteLine("{0}: {1}", Thread.CurrentThread.ManagedThreadId, String.Format(format, args));
    }

    public async Task UIAction()
    {
        // UI Thread
        Log("UIAction");

        // start the CPU-bound work
        var cts = new CancellationTokenSource(5000);
        var workTask = DoWorkAsync(cts.Token); 

        // possibly await for some IO-bound work 
        await Task.Delay(1000);
        Log("after Task.Delay");

        // finally, get the result of the CPU-bound work
        int c = await workTask;
        Log("Result: {0}", c);
    }

    async Task<int> DoWorkAsync(CancellationToken ct)
    {
        // start on the UI thread
        Log("DoWorkAsync");

        // switch to a pool thread and yield back to the UI thread
        await SwitchContext.Yield();
        Log("after SwitchContext.Yield");
        // continue on a pool thread

        int c = 0;
        while (!ct.IsCancellationRequested)
        {
            // do some CPU-bound work on a pool thread: counting cycles :)
            c++;
            // and use async/await too
            await Task.Delay(50);
        }

        return c;
    }

}

Теперь, без SwitchContext.Yield, DoWorkAsync будет выглядеть ниже. Он добавляет некоторый дополнительный уровень сложности в виде делегата async и вложенности задачи:

async Task<int> DoWorkAsync(CancellationToken ct)
{
    // start on the UI thread
    Log("DoWorkAsync");

    // Have to use async delegate
    // Task.Run uwraps the inner Task<int> task
    return await Task.Run(async () =>
    {
        // continue on a pool thread
        Log("after Task.Yield");

        int c = 0;
        while (!ct.IsCancellationRequested)
        {
            // do some CPU-bound work on a pool thread: counting cycles :)
            c++;
            // and use async/await too
            await Task.Delay(50);
        }

        return c;
    });
}

Тем не менее, реализация SwitchContext.Yield может быть довольно простой и (смею сказать) эффективной:

public static class SwitchContext
{
    public static Awaiter Yield() { return new Awaiter(); }

    public struct Awaiter : System.Runtime.CompilerServices.INotifyCompletion
    {
        public Awaiter GetAwaiter() { return this; }

        public bool IsCompleted { get { return false; } }

        public void OnCompleted(Action continuation)
        {
            ThreadPool.QueueUserWorkItem((state) => ((Action)state)(), continuation);
        }

        public void GetResult() { }
    }
}

Итак, мой вопрос, почему я должен предпочесть вторую версию DoWorkAsync по сравнению с первой, и почему использование SwitchContext.Yield считается плохой практикой?

Ответ 1

Вам не нужно вставлять Task.Run в DoWorkAsync. Рассмотрим эту опцию:

public async Task UIAction()
{
    // UI Thread
    Log("UIAction");

    // start the CPU-bound work
    var cts = new CancellationTokenSource(5000);
    var workTask = Task.Run(() => DoWorkAsync(cts.Token)); 

    // possibly await for some IO-bound work 
    await Task.Delay(1000);
    Log("after Task.Delay");

    // finally, get the result of the CPU-bound work
    int c = await workTask;
    Log("Result: {0}", c);
}

Это приводит к коду с гораздо более ясными намерениями. DoWorkAsync является естественным синхронным методом поэтому он имеет синхронную подпись. DoWorkAsync не знает и не заботится о пользовательском интерфейсе. UIAction, который заботится о потоке пользовательского интерфейса, отталкивает работу на фоновый поток, используя Task.Run.

Как правило, попробуйте "нажимать" любые Task.Run вызовы из ваших методов библиотеки как можно больше.