Giter VIP home page Giter VIP logo

asyncenumerator's Introduction

Task-like Async Enumerators

a.k.a Abusing Task-like Types in C# 7

Since async/await was first released many have craved a corresponding 'async iterator' approach that could blend both the yield and async syntaxes.

While there are several options currently available for asynchronous sequences (Rx Observables, DataFlow blocks), none have the concise beauty of async/await and yield iterators.

Async sequences are on the book for C# 8, and there are a number of interesting discussions happening in the Rosyln issues.

But why wait - with C# 7's new Task-Like types we finally have the possibility to create async methods that return custom types. By capturing the underlying task-like object from within the method itself, we can return values before the task has completed.

Mads Torgersen has repeated that very few people will create Task-Like types, but I believe the approach shown here will have many cool applications. This repository contains my initial proof of concept.

Warning!

I put this together as an experiment, there are likely better ways to approach most of the inner workings. I put zero emphasis on correctness, safety, or performance.

Details

This repository contains several Task-like types that allow both cooporative and parallel async iterator methods.

AsyncEnumerator<T>

The AsyncEnumerator<T> class provides behavior similar to a standard yield iterator method except that it allows for asynchronous operations. Each yield.Return(T) call returns a value and asynchronously waits for the next call to MoveNextAsync():

public static async AsyncEnumerator<int> Producer()
{
    var yield = await AsyncEnumerator<int>.Capture(); // Capture the underlying 'Task'

    await yield.Pause();             // Optionally wait for the first MoveNext call

    await yield.Return(1);           // Yield the value and await MoveNext
                   
    await Task.Delay(100);           // Use any async constructs

    await yield.Return(2);

    return yield.Break();            // Return false to awaiting MoveNext
}

public static async Task Consumer()
{
    var p = Producer();                       

    while (await p.MoveNextAsync())       // Await the next value
    {
        Console.Write(" " + p.Current);   // Use the current value
    }
}

AsyncSequence<T>

While cooperative iteration is great, I believe the more common desire with async code would be to have the producer run in parallel and simply await the availability of results (closer in concept to observables). The AsyncSequence<T> class shown below accomplishes this goal:

public static async AsyncSequence<int> Producer2()
{
    var seq = await AsyncSequence<int>.Capture();            // Capture the underlying 'Task'
                       
    var users = await GetUsersAsync().ConfigureAwait(false); // Use any async constructs
    
    foreach(var user in users)
    {
        var fiends = await user.GetFriendsAsync(); // Build async 'flows' naturally 
        
        seq.Return(friends.Count);            // Signal an awaiting 
    }                                         // MoveNext, or queue the result.

    return seq.Break();                       // Complete the sequence and 
}                                             // return false to an awaiting MoveNext

public static async Task Consumer2()
{
    var p = Producer2();

    while (await p.MoveNextAsync())     // Await the next value
    {
        Console.WriteLine(p.Current);   // Use the current value
    }
}

Other Types

I also threw together several additional types with a similar approach. One is a TaskLikeObservable which allows you to write a flat and async IObservable method such as:

public static async TaskLikeObservable<string> Producer()
{
    var o = await TaskLikeObservable<string>.Capture(); // Capture the underlying Task-like Obserable

    await o.Subscription;                               // wait for a subscriber

    for (var i = 0; i < 10; i++)
    {
        await Task.Delay(100).ConfigureAwait(false);    // Use normal async constructs
        
        o.OnNext("y" + i);                              // send the value
    }

    return o.OnCompleted();                             // complete the observable and return.
}

...

Producer().Subscribe(i => DoSomethingWith(i));          // Run and subscribe to the method

Lastly I threw in a CoopTask class which allows a parent and child task to pass control back and forth similarly to a yielding enumerator:

public static async CoopTask Child()
{
    var task = await CoopTask.Capture();          // Capture the underlying 'Task'

    Console.WriteLine("P0");

    await task.Yield();                           // Yield control back to parent

    await Task.Delay(100).ConfigureAwait(false);  // Use any async constructs

    Console.WriteLine("P1");

    await task.Yield();                           // Yield control

    await Task.Delay(100);

    Console.WriteLine("P2");

    await task.Break();                           // Mark the task as completed

    Console.WriteLine("P3");                      // Will not be run.
}

public static async Task Parent()
{
    var p = Child();

    var i = 1;

    while (await p.MoveNextAsync())          // Await the next child operation
    {
        Console.WriteLine("C" + i++);        
    }
}

Discussion

There are likely better ways of implementing most of the internals of these types. For one thing, I largely used classes over structs (which are used by the standard framework types) to allow for more code reuse.

The 'hack' for capturing the underlying Task-like object is not as bad as I thought it would have to be - I originally used reflection, then found the current method which only relies on a 'dummy' continuation.

The main oddity I have run into is that the compiler appears to treat all generic Task-like methods as if they should behave like Task<T> even when their builder semantics actually follow the void returning Task approach. This means you have to 'return' a value even when the Task and awaiter have no Result field. I guess this wasn't the intended usage...

I will be very curious to hear other developer's feedback. Please leave comments or feedback as issues. And of course, I welcome any pull requests for better code or more functionality.

asyncenumerator's People

Contributors

andrew-hanlon avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.