Monday 26 June 2017

Local Functions and Async

Following my previous post on Local Functions and Iterator methods, it's time now to see what I've learnt about async thanks to local functions

When reading the last part of this article, this paragraph was a great finding.

This exhibits the same issue as the iterator method. This method doesn’t synchronously throw exceptions, because it is marked with the ‘async’ modifier. Instead, it will return a faulted task. That Task object contains the exception that caused the fault. Calling code will not observe the exception until the Task returned from this method is awaited (or its result is examined).

So, let's compare these 2 methods:

public static Task<string> DoProcess1(string st)
  {
   Console.WriteLine("DoProcess1method started in: " +  Thread.CurrentThread.ManagedThreadId);
   if (st.StartsWith("throw")){
    throw new Exception("Can not process invalid string");
   }
   Console.WriteLine("string is valid to be processed");
   
   
   var t1 = Task.Run(() => {
                      Console.WriteLine("Time consuming operation running in: " +  Thread.CurrentThread.ManagedThreadId);
                      Thread.Sleep(1000);
                      return st.ToUpper();
                     });
   return t1;
  
  }
  
  public static async Task<string> DoProcess2(string st)
  {
   Console.WriteLine("DoProcess2 method started in: " +  Thread.CurrentThread.ManagedThreadId);
   if (st.StartsWith("throw")){
    throw new Exception("Can not process invalid string");
   }
   Console.WriteLine("string is valid to be processed");
   
   
   var res = await Task.Run(() => {
                      Console.WriteLine("Time consuming operation running in: " +  Thread.CurrentThread.ManagedThreadId);
                      Thread.Sleep(1000);
                      return st.ToUpper();
                     });
   
   return res;
  
  }

Both methods are basically the same, but the second is using await and hence we've had to decorate it with the async keyword. In both cases the first lines of the method (until Task.Run) are executed by the thread from the caller method, but while in Process1, the exception will be thrown immediatelly, in Process2 the compiler generated code will capture the exception and won't throw it until the Result of the returned Task is accessed. Let's see:

try{
    t1 = ProcessingService3.DoProcess1("throw aaaa");
    
    //we DON't reach this line
    Console.WriteLine("after Process1 call");
    Console.WriteLine(t1.Result);
   }
   catch (Exception ex){
    Console.WriteLine("Exception: " + ex);
   }
   
   Console.WriteLine("----------------");
   
   try{
    t1 = ProcessingService3.DoProcess2("throw bbbb");
    
    //we reach this line
    Console.WriteLine("after Process1 call");
    //exception occurs in the line below, when accessing Result
    Console.WriteLine(t1.Result);
   }
   catch (Exception ex){
    Console.WriteLine("Exception: " + ex);
   }

This is interesting, in DoProcess1, 0 or 1 Task object is created (depending on whether we throw or not before reaching Task.Run). In a method marked as async, like DoProcess2 the compiler creates at least 1 "main" Task object. This main Task object is always created, it does not involve a particular thread, but is used to hold the Result/Exceptions of the different Tasks created inside the method, and is returned to the invoker of the method. If DoProcess2 does not throw an Exception then it reaches Task.Run, and the compiler will add an invocation to ContinueWith on that second task, and so on for as many "await" calls we could have inside the async method. For the last one the ContinueWith will set a Result in that main Task created at the beginning of the call. If at some point an exeption happens the exception will be added to the Main Task and no more code of the asynchronous method will be run.

OK, the explanation above is confusing and not really accurate, but it helps me to get an approximate idea of what is going on in a method marked as async. If you really want to understand it, check this amazing post.

As you can see in that glorious article, for each method marked as async the compiler will create a class that is basically a State Machine. This State Machine has a MoveNext method that orchestrates the sequence of asynchronous calls. Each await call that exists in the async method is like a step, and where each awaited Task is asked (via ContinueWith) to call into MoveNext once it's done.

It's amazing to see how similar this compiler generated State Machine is to the one generated for an Iterator method. In the end, both things are basically the same, the big difference is that for Iterators is the consumer of the iterator who will be calling to MoveNext, while for the async-await thing it's the asynchronous method who once finished will call to MoveNext. Just as a reminder, in ES6 (while waiting for the await keyword to be introduced in ES7) people have been leveraging generators to sort of simulate await and avoid the callback hell.

No comments:

Post a Comment