Saturday 5 October 2019

C# Asynchronous (pull) Streams

C# 8 is finally out, with two main, long awaited features: Default Interface Methods and Asynchronous (Pull) Streams (Asynchronous Enumeration). Regarding the latter, already 3 years ago I had posted about how useful it would be to have something like that, and I think it will be good to do a recap (I also posted about types of streams early this year).

At that time I had said that in some cases we could deal with Asynchronous Enumeration by returning an IEnumerable<Task<T>>. This technique is only valid when knowing if we can get a new element or not (so MoveNext returns true or false) is synchronous, and the asynchronous part is obtaining the value Task<T> that will be set in Current. An example:

 private static Task<string> GetBlogAsync(int id)
 {
  return Task.Delay(500).ContinueWith(task => "Blog " + id.ToString());
 }


 private static IEnumerable<Task<string>> GetBlogs()
 {
  for (var i=0; i<5; i++)
  {
   yield return GetBlogAsync(i);
  }
 }
 

 static async Task Main(string[] args)
 {
  foreach (var blogTask in GetBlogs())
  {
   Console.WriteLine(blogTask.Result);
  }
 }

The above code can be written in C# 8 (thanks to the introduction of IAsyncEnumerable and IAsyncEnumerator) like this:

        private static async IAsyncEnumerable<string> GetBlogs2()
        {
            for (var i=0; i<5; i++)
            {
                yield return await GetBlogAsync(i);
            }
        }
  
  static async Task Main(string[] args)
        {
            await foreach (var blog in GetBlogs2())
            {
                Console.WriteLine(blog);
            }
        }
    }

For this case, where we can know if we have reached the end of the iteration (i==5) without trying to get the element, both options are equally valid, but we have to understand that there's a difference in what we are doing. The difference between IEnumerable<Task<T>> and IAsyncEnumerable<T> is that for the former, MoveNext() returns a bool, and Current returns a Task<T>. For the latter, MoveNextAsync() returns a Task<bool> and Current returns T. Thanks to this, IAsyncEnumerable works fine both when the iteration end condition is known synchronously or asynchronously.

Modern javascript also includes this feature, known as Asynchronous Iterators, and this article makes a really good read about it. By the way, I've always found it rather confusing that the Iterator protocol uses Symbol.iterator rather than Symbol.getIterator for the name of the function that defines an object as iterable and returns the iterator. It is a function, so it should have a verb name, not a noun...

No comments:

Post a Comment