C# 5 async: experimenting with member resolution (GetAwaiter, BeginAwait, EndAwait)

Some of you may remember the bizarre query expressions I’ve come up with before now. These rely on LINQ finding the members it needs (Select, Where, SelectMany etc) statically but without relying on any particular interface. I was pleased to see that the C# 5 async support is based on the same idea. Here’s the relevant bit of the draft spec:

The expression t of an await-expression await t is called the task of the await expression. The task t is required to be awaitable, which means that all of the following must hold:

  • (t).GetAwaiter() is a valid expression of type A.
  • Given an expression a of type A and an expression r of type System.Action, (a).BeginAwait(r) is a valid boolean-expression.
  • Given an expression a of type A, (a).EndAwait() is a valid expression.

A is called the awaiter type for the await expression. The GetAwaiter method is used to obtain an awaiter for the task.

The BeginAwait method is used to sign up a continuation on the awaited task. The continuation passed is the resumption delegate, which is further explained below.

The EndAwait method is used to obtain the outcome of the task once it is complete.

The method calls will be resolved syntactically, so all of GetAwaiter, BeginAwait and EndAwait can be either instance members or extension methods, or even bound dynamically, as long as the calls are valid in the context where the await expression appears. All of them are intended to be “non-blocking”; that is, not cause the calling thread to wait for a significant amount of time, e.g. for an operation to complete.

As far as I can tell, either the CTP release hasn’t fully implemented this, or I’ve interpreted a bit overly broadly. Still, let’s see what works and what doesn’t. For simplicity, each example is completely self-contained… and does absolutely nothing interesting. It’s only the resolution part which is interesting. (The fact that the Main method is async is quite amusing though, and takes advantage of the fact that async methods can return void instead of a task.)

Example 1: Boring instance members

This is closest to the examples I’ve given so far.

using System;

class Test
{
    static async void Main()
    {
        await new Awaitable();
    }
}

class Awaitable
{
    public Awaiter GetAwaiter()
    {
        return new Awaiter();
    }    
}

class Awaiter
{
    public bool BeginAwait(Action continuation)
    {
        return false;
    }
    
    public int EndAwait()
    {
        return 1;
    }
}

Hopefully this needs no further explanation. Obviously it works fine with the CTP. The compiler generates a call to GetAwaiter, then calls BeginAwait and EndAwait on the returned Awaiter.

Example 2: Extension methods

The CTP uses extension methods to get an awaiter for existing types such as Task – but I don’t think it uses them for BeginAwait/EndAwait. Fortunately, there’s nothing to stop us from using them for everything, and there’s nothing forcing you to put the extension methods on sensible types, either – as demonstrated below:

using System;

class Test
{
    static async void Main()
    {
        Guid guid = await 5;
        Console.WriteLine("Got result: {0}", guid);
    }
}

static class Extensions
{
    public static string GetAwaiter(this int number)
    {
        return number.ToString();
    }
    
    public static bool BeginAwait(this string text, Action continuation)
    {
        Console.WriteLine("Waiting for {0} to finish", text);
        return false;
    }
    
    public static Guid EndAwait(this string text)
    {
        Console.WriteLine("Finished waiting for {0}", text);
        return Guid.NewGuid();
    }
}

I should just emphasize that this code is purely for the sake of experimentation. If I ever see anyone actually extending int and string in this way in production code and blaming me for giving them the idea, I’ll be very cross.

However, it all does actually work. This example is silly but not particularly exotic. Let’s start going a bit further, using dynamic typing.

Example 3: Dynamic resolution

The spec explicitly says that the methods can be bound dynamically, so I’d expect this to work:

using System;
using System.Dynamic;

class Test
{
    static async void Main()
    {
        dynamic d = new ExpandoObject();
        d.GetAwaiter = (Func<dynamic>) (() => d);
        d.BeginAwait = (Func<Action, bool>) (action => {
            Console.WriteLine("Awaiting");
            return false;
        });
        d.EndAwait = (Func<string>)(() => "Finished dynamically");

        string result = await d;
        Console.WriteLine("Result: {0}", result);
    }
}

Unfortunately, in the CTP this doesn’t work – it fails at compile time with this error:

Test.cs(16,25): error CS1991: Cannot await ‘dynamic’

All is not lost, however. We may not be able to make GetAwaiter to be called dynamically, but what about BeginAwait/EndAwait? Let’s try again:

using System;
using System.Dynamic;

class DynamicAwaitable
{
    public dynamic GetAwaiter()
    {
        dynamic d = new ExpandoObject();
        d.BeginAwait = (Func<Action, bool>) (action => {
            Console.WriteLine("Awaiting");
            return false;
        });
        d.EndAwait = (Func<string>)(() => "Finished dynamically");
        return d;
    }
}

class Test
{
    static async void Main()
    {
        string result = await new DynamicAwaitable();
        Console.WriteLine("Result: {0}", result);
    }
}

This time we get more errors:

Test.cs(22,25): error CS1061: ‘dynamic’ does not contain a definition for ‘BeginAwait’ and no extension method ‘BeginAwait’ accepting a first argument of type ‘dynamic’ could be found (are you missing a using directive or an assembly reference?)

Test.cs(22,25): error CS1061: ‘dynamic’ does not contain a definition for ‘EndAwait’ and no extension method ‘EndAwait’ accepting a first argument of type ‘dynamic’ could be found (are you missing a using directive or an assembly reference?)

Test.cs(22,25): error CS1986: The ‘await’ operator requires that its operand ‘DynamicAwaitable’ have a suitable public GetAwaiter method

This is actually worse than before: not only is it not working as I’d expect to, but even the error message has a bug. The await operator doesn’t require that its operand has a suitable public GetAwaiter method – it just has to be accessible. At least, that’s the case with the current CTP. In my control flow post for example, the methods were all internal. It’s possible that the error message is by design, and the compiler shouldn’t have allowed that code, of course – but it would seem a little odd.

Okay, so dynamic resolution doesn’t work. Oh well… let’s go back to static typing, but use delegates, fields and properties.

Example 4: Fields and properties of delegate types

This time we’re back to the style of my original "odd query expressions" post, using fields and properties returning delegates instead of methods:

using System;

class FieldAwaiter
{
    public readonly Func<Action, bool> BeginAwait = continuation => false;
    public readonly Func<string> EndAwait = () => "Result from a property";
}

class PropertyAwaitable
{
    public Func<FieldAwaiter> GetAwaiter
    {
        get { return () => new FieldAwaiter(); }
    }
}

class Test
{
    static async void Main()
    {
        string result = await new PropertyAwaitable();
        Console.WriteLine("Result: {0}", result);
    }
}

Again, I believe this should work according to the spec. After all, this block of code compiles with no problems:

var t = new PropertyAwaitable();
var a = (t).GetAwaiter();
bool sync = (a).BeginAwait(() => {});
string result = (a).EndAwait();

Unfortunately, nothing doing. The version using await fails with this error:

Test.cs(21,25): error CS1061: ‘PropertyAwaitable’ does not contain a definition for ‘GetAwaiter’ and no extension method ‘GetAwaiter’ accepting a first argument of type ‘PropertyAwaitable’ could be found (are you missing a using directive or an assembly reference?)

Test.cs(21,25): error CS1986: The ‘await’ operator requires that its operand ‘PropertyAwaitable’ have a suitable public GetAwaiter method

This wasn’t trying truly weird things like awaiting a class name. Oh well :(

Conclusion

Either I’ve misread the spec, or the CTP doesn’t fully comply to it. This should come as no surprise. It’s not a final release or even a beta. However, it’s fun to investigate the limits of what should be valid. The next question is whether the compiler should be changed, or the spec… I can’t immediately think of any really useful patterns involving returning delegates from properties, for example… so is it really worth changing the compiler to allow it?

 

Update (7th November 2010)

On Thursday I spoke to Mads Torgersen and Lucian Wischik about this. Some changes being considered:

  • The C# spec being tightened up to explicitly say that GetAwaiter/BeginAwait/EndAwait have to be methods, at least when statically typed. In other words, the delegate/property version wouldn’t be expected to work.
  • The BeginAwait pattern may be tightened up to require a return type of exactly Boolean or dynamic (rather than the call being "a boolean-expression" which is very slightly more lax)
  • The dynamic version working – this is more complicated in terms of implementation than one might expect

Just to emphasize, these are changes under consideration rather than promises. They seem entirely reasonable to me. (Dynamic binding sounds potentially useful; property/field resolution definitely less so. Making the spec match the implementation is important to me though :)

11 thoughts on “C# 5 async: experimenting with member resolution (GetAwaiter, BeginAwait, EndAwait)”

  1. For the dynamic resolution example, that’s probably covered by point 4.6 of the known issues that are displayed after installing the CTP – “Late-bound await expressions are not yet implemented in the current CTP”.

    On the subject of experimenting with wacky stuff with this new feature, one part of the spec that caught my attention was:
    “If BeginAwait returns true, the awaiter should make sure that the delegate r is invoked at most once. If BeginAwait returns false, the awaiter should make sure that the delegate r is never invoked. If r is invoked beyond these restrictions, its behavior is undefined.”

    Undefined behaviour is always fun to experiment with. With my initial experiments, I’ve found that I can get anonymous methods to continue beyond awaiting a task that is not yet complete, but I’m sure we can find some even more interesting things for it to do.

    Like

  2. @Michael: What I’ve found interesting is that the continuation action is a MoveNext() method. It might be fun to build an iterator from the generated type, and see what happens :)

    Like

  3. If anyone is interested, here is a dumb implementation of my previous comment that allow syntaxes like that to work :

    await 5.Seconds();
    await TimeSpan.FromMinutes(42);
    await (DateTime.Now + 5.Seconds());
    await new DateTime(2015, 10, 21);

    I don’t know if anything like that should end in production software, but the fact that it is possible is pretty cool.

    public static class Test
    {
    public static TimeSpan Seconds(this int value)
    {
    return TimeSpan.FromSeconds(value);
    }

    public static TaskAwaiter GetAwaiter(this TimeSpan timespan)
    {
    return TaskEx.Delay(timespan).GetAwaiter();
    }

    public static TaskAwaiter GetAwaiter(this DateTime date)
    {
    return (date – DateTime.Now).GetAwaiter();
    }
    }

    Like

  4. These conclusions are not so surprising considering that foreaching over an object having a Func<IEnumerator> GetEnumerator property or an expando object with such a property do not work either.

    Like

  5. @Trillian: On the contrary: the language specification calls out the fact that it has to be a GetEnumerator *method* on the relevant type. Compare that with the piece of specification I quoted, which only talks about the validity of certain expressions, and *specifically* calls out the ability for these to be bound dynamically.

    Like

  6. Of course it’s useful. What if you want to want on a task created from IronPython, using dynamic objects?

    What if you want to wait on a task created from a new hypothetical language which doesn’t have methods but has fields of delegate types? (Yes, that language wouldn’t be CLS compliant, but still)

    Like

  7. On the subject of whacky C# code, this is a valid 4.0 function declaration.

    dynamic dynamic( dynamic dynamic )
    {
    return 0;
    }

    Like

  8. @Tanveer: If you change it just a bit it can actually become useful!

    dynamic dynamic(dynamic dynamic)
    {
    return dynamic;
    }

    And now you have a slightly-nicer syntax for dynamic calls: instead of calling Method((dynamic)param), you can now call Method(dynamic(param)).

    I’d make it an extension method but that would ruin the beauty of it :P

    Like

  9. GetAwaiter property and delegate fields do not work because this is a .NET (and not C#) feature. GetAwaiter property in C# is get_GetAwaiter() method in .NET IL, so GetAwaiter() method is not found. Hence the error.

    Regarding IronPython on any language that does not support methods in general or GetAwaiter() method in particular – whatever it supports could be invoked from a C# shim GetAwaiter() method, thereby extending this feature to such languages/frameworks.

    Like

  10. @Vishal: No, the call to GetAwaiter is added by the C# compiler, which would be perfectly capable of calling a delegate via a property – just as it does with LINQ. The spec is currently phrased in terms of valid expressions, and objectWithProperty.GetAwaiter() is a perfectly valid expression. Having spoken with Mads, I expect this *won’t* work in the final version, but the spec will hopefully be tightened up. Note that it’s the C# specification which is in play here, not any .NET specification. While tasks etc are .NET features, the behaviour of the “await” contextual keyword is firmly a language feature. For example, C# and VB *could* behave differently in this regard (although I don’t expect them to).

    I believe the dynamic binding feature *will* be supported.

    Like

Leave a comment