Task.Yield() в сравнении с Task.Delay(0)

Всегда ли Delay(0) встраивается? По моему опыту, он:

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

namespace ConsoleApplication
{
    class Program
    {
        static async Task Test()
        {
            await Task.Yield();
            Console.WriteLine("after Yield(), thread: {0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(0);
            Console.WriteLine("after Delay(0), thread: {0}", Thread.CurrentThread.ManagedThreadId);
            await Task.Delay(100);
            Console.WriteLine("after Delay(100), thread: {0}", Thread.CurrentThread.ManagedThreadId);
        }
        static void Main(string[] args)
        {
            Console.WriteLine("Main thread: {0}", Thread.CurrentThread.ManagedThreadId);
            Test().Wait();
        }
    }
}

Это консольное приложение, поэтому пул потоков используется для продолжения. Выход:

Main thread: 11
after Yield(), thread: 7
after Delay(0), thread: 7
after Delay(100), thread: 6

Ответ 1

Внутри Task.Delay он выглядит следующим образом (единственный параметр (int) просто вызывает следующую версию):

[__DynamicallyInvokable]
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
{
    if (millisecondsDelay < -1)
    {
        throw new ArgumentOutOfRangeException("millisecondsDelay", Environment.GetResourceString("Task_Delay_InvalidMillisecondsDelay"));
    }
    if (cancellationToken.IsCancellationRequested)
    {
        return FromCancellation(cancellationToken);
    }
    if (millisecondsDelay == 0)
    {
        return CompletedTask;
    }
    DelayPromise state = new DelayPromise(cancellationToken);
    if (cancellationToken.CanBeCanceled)
    {
        state.Registration = cancellationToken.InternalRegisterWithoutEC(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state);
    }
    if (millisecondsDelay != -1)
    {
        state.Timer = new Timer(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state, millisecondsDelay, -1);
        state.Timer.KeepRootedWhileScheduled();
    }
    return state;
}

Как вы можете с уверенностью видеть:

    if (millisecondsDelay == 0)
    {
        return CompletedTask;
    }

Это означает, что он всегда возвращает завершенную задачу, поэтому ваш код всегда будет продолжать проходить через эту конкретную строку await.

Ответ 2

Да, да. Проверка IL в отражателе показывает (среди прочего логику):

if (millisecondsDelay == 0)
{
    return CompletedTask;
}

Итак, да, в этом случае он вернет вам уже выполненную задачу.

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

Возвращение уже завершенной задачи - рекомендуемый трюк, когда ответ уже известен/доступен синхронно; также часто используется кеш Task для общих значений результата.