How is Task.Delay awaitable if it's not marked async?
Asked Answered
L

1

7

I'm looking at Task.Delay(int) decompiled in ILSpy:

// System.Threading.Tasks.Task
[__DynamicallyInvokable]
public static Task Delay(int millisecondsDelay)
{
    return Task.Delay(millisecondsDelay, default(CancellationToken));
}

This method is used like await Task.Delay(5000);, and the intellisense even says "(awaitable)":

enter image description here

So how is it that Task.Delay(int) isn't marked async (public static async Task Delay(int millisecondsDelay))?

Lansquenet answered 30/1, 2015 at 13:17 Comment(4)
It's tasks which are awaitable, not the functions that return them.Ljoka
Async/Await FAQ: "An “awaitable” is any type that exposes a GetAwaiter method which returns a valid “awaiter”. This GetAwaiter method may be an instance method (as it is in the case of Task and Task<TResult>), or it may be an extension method."Parshall
@I3arnon -- You edited to add c# tag, but this isn't specifically a c# question (that's just the language that ILSpy shows). I could use Task.Delay from VB.NET.Lansquenet
@roryap it isn't, but C# (and .Net) are an umbrella tag regarding the entire MS stack. It also tells the formatter to how to format the code without hints. And to be technical, the question isn't necessarily about the TPL (since everything can be awaited) or .Net (since you can use await outside of it, e.g. WinRT).Tail
T
19

What's awaitable is the Task Task.Delay returns. Each method returning a Task/Task<TResult> is awaitable. async is just an implementation detail allowing you to use await in that method and the whole state machine it generates.

More generally, every thing that has a GetAwaiter method (extension methods count as well) that return something that has IsCompleted, OnCompleted and GetResult can be awaited.

For example, Task.Yield returns YieldAwaitable which isn't a Task and looks like this:

public struct YieldAwaiter : ICriticalNotifyCompletion, INotifyCompletion
{
    public void OnCompleted(Action continuation);
    public void UnsafeOnCompleted(Action continuation);
    public void GetResult();
    public bool IsCompleted { get; }
}

*UnsafeOnCompleted here is just an optimization, await would work without it.

It's important to note that the compiler in this case (same as in other cases like GetEnumerator for foreach) doesn't expect an interface or a base class. It basically uses duck typing (i.e. "if it walks like a duck...") and simply looks for a GetAwaiter method that returns anything (doesn't matter what type or interface or if it's a class or a struct) that has the other 3 members (IsCompleted, OnCompleted and GetResult)

For example, this is how you can make await "bar" compile (it will fail in runtime of course):

public static Awaiter GetAwaiter(this string s)
{
    throw new NotImplementedException();
}
public abstract class Awaiter : INotifyCompletion
{
    public abstract bool IsCompleted { get; }
    public abstract void GetResult();
    public abstract void OnCompleted(Action continuation);
}

In conclusion, you don't need async to return an awaitable and moreover most Task-returning methods in the .Net framework don't use it and explicitly return a Task.

Tail answered 30/1, 2015 at 13:23 Comment(4)
Nice. So, without looking in to it yet -- is there an interface that Task or anything else implements that has a GetAwaiter method and has IsCompleted, OnCompleted, and GetResult? In other words, how is that achieved?Lansquenet
Shouldn't the awaitable implement INotifyCompletion or ICriticalNotifyCompletion?Theatrical
@roryap - Look into INotifyCompletionTheatrical
@roryap in general, no. Task has GetAwaiter without an interface. The awaiter itself (e.g. TaskAwaiter) implements INotifyCompletion (and sometimes ICriticalNotifyCompletion) but it doesn't need to. I'll add that to the answer.Tail

© 2022 - 2024 — McMap. All rights reserved.