20

Does Delay(0) always get inlined? In my experience, it does:

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();
        }
    }
}

This is a console app, thus the thread pool is used for continuation. The output:

Main thread: 11
after Yield(), thread: 7
after Delay(0), thread: 7
after Delay(100), thread: 6
noseratio
  • 59,932
  • 34
  • 208
  • 486

2 Answers2

26

Inside Task.Delay, it looks like this (the single parameter (int) version just calls the below version):

[__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;
}

As you can hopefully see:

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

Which means it always returns a completed task, and therefore your code will always continue running past that particular await line.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
12

Yes, it does. A check of the IL in reflector shows (among other logic):

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

So yes, it will hand you back an already-completed task in this case.

Note that the implementation of await includes checks that ensure that an already-completed task doesn't cause an additional context switch, so yes: your code will keep running without pausing for breath here.

Returning an already completed task is a recommended trick when the answer is already known / available synchronously; it is also common to cache Tasks for common result values.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Great, thanks! Looks to me like a handy way to create a non-generic [task in completed state](http://stackoverflow.com/a/18527377/1768303). Accepting Damien's answer as technically he was first :] – noseratio Aug 30 '13 at 07:51
  • 1
    @Noseratio I think using something like `Task completed = Task.FromResult(true);` is better, because it is guaranteed to work. I think `Task.Delay(0)` is not required to return a completed `Task`. – svick Aug 30 '13 at 10:55
  • @svick, I agree `Task.FromResult(true)` is more appropriate, but I still like `Task.Delay(millisecondsDelay: 0)` as I can easily simulate both sync and async continuation just by changing `millisecondsDelay`. Do you think they might change this behavior? That'd look like a breaking change for me, giving the above code. – noseratio Aug 30 '13 at 11:05
  • 3
    @Noseratio The documentation doesn't clearly say what exactly happens for `0`. And I think you can't really consider a change of undocumented behavior to be "breaking" (even if it breaks programs that rely on it). – svick Aug 30 '13 at 11:14
  • Just to be clear -- a No-Op is not the same as an inlined function (did I misunderstand the question?) -- a function with multiple returns / if statements (as shown in either answer) will never be "inlined" by .NET. – BrainSlugs83 Dec 12 '13 at 20:17
  • Hi, just a question: can we always replace Task.Delay(0) with Task.CompletedTask? I think latter is clearer. – Sheen Apr 03 '16 at 21:30
  • @Sheen No. You are not able to do so with .NET 4.5. `Task.CompletedTask` only available in .NET 4.6. But if you have access to it then of cause you should use it. – Earth Engine Apr 12 '16 at 03:13
  • @Noseratio I personally prefer `Task.FromResult(0)` simply because it is slightly shorter (and probably the shortest) . – Earth Engine Apr 12 '16 at 03:15