Asynchrony in C # and F #. Asynchrony pitfalls in C #

Hello, Habr! I present to your attention the translation of the article "Async in C # and F # Asynchronous gotchas in C #" by Tomas Petricek.



Back in February, I attended the Annual MVP Summit, an event hosted by Microsoft for MVPs. I took this opportunity to visit Boston and New York as well, do two talks on F #, and record Channel 9's lecture on type providers . Despite other activities (such as visiting pubs, chatting with other people about F #, and taking long naps in the morning), I was also able to have a few discussions.



image



One discussion (not under the NDA) was the Async Clinic talk about the new keywords in C # 5.0 - async and await. Lucian and Stephen talked about common problems that C # developers face when writing asynchronous programs. In this post, I will look at some of the problems from an F # perspective. The conversation was quite lively, and someone described the reaction of the F # audience as follows:



image

(When MVPs writing in F # see C # code examples, they giggle like girls)



Why is this happening? It turns out that many of the common errors are impossible (or much less likely) when using the F # asynchronous model (which appeared in F # 1.9.2.7, released in 2007 and shipped with Visual Studio 2008).



Pitfall # 1: Async does not work asynchronously



Let's jump straight to the first tricky aspect of the C # asynchronous programming model. Take a look at the following example and try to imagine in what order the lines will be printed (I couldn't find the exact code shown in the talk, but I remember Lucian demonstrating something similar):



  async Task WorkThenWait()
  {
      Thread.Sleep(1000);
      Console.WriteLine("work");
      await Task.Delay(1000);
  }
 
  void Demo() 
  {
      var child = WorkThenWait();
      Console.WriteLine("started");
      child.Wait();
      Console.WriteLine("completed");
  }


If you think that "started", "work" and "completed" will be printed, you are wrong. The code prints "work", "started" and "completed", try it yourself! The author wanted to start working (by calling WorkThenWait) and then wait for the task to complete. The problem is that WorkThenWait starts by doing some heavy computation (here Thread.Sleep) and only after that it uses await.



In C #, the first piece of code in an async method runs synchronously (on the caller's thread). You can fix this, for example, by adding await Task.Yield () at the beginning.



Corresponding F # code



In F #, this is not a problem. When writing asynchronous code in F #, all code inside the async {…} block is deferred and run later (when you explicitly run it). The above C # code corresponds to the following in F #:



let workThenWait() = 
    Thread.Sleep(1000)
    printfn "work done"
    async { do! Async.Sleep(1000) }
 
let demo() = 
    let work = workThenWait() |> Async.StartAsTask
    printfn "started"
    work.Wait()
    printfn "completed"
  


Obviously, the workThenWait function does not perform the work (Thread.Sleep) as part of the asynchronous computation, and that it will execute when the function is called (and not when the asynchronous workflow starts). A common pattern in F # is to wrap the entire body of a function in async. In F #, you would write the following, which works as expected:



let workThenWait() = async
{ 
    Thread.Sleep(1000)
    printfn "work done"
    do! Async.Sleep(1000) 
}
  


Pitfall # 2: Ignoring Results



Here's another problem with the C # asynchronous programming model (this article is taken directly from Lucian's slides). Guess what happens when you run the following asynchronous method:



async Task Handler() 
{
   Console.WriteLine("Before");
   Task.Delay(1000);
   Console.WriteLine("After");
}
 


Do you expect it to print "Before", wait 1 second, and then print "After"? Wrong! Both messages will be printed at once, without intermediate delay. The problem is that Task.Delay returns a Task, and we forgot to wait until it completes (using await).



Corresponding F # code



Again, you probably wouldn't have encountered this in F #. You may well write code that calls Async.Sleep and ignores the returned Async:



let handler() = async
{
    printfn "Before"
    Async.Sleep(1000)
    printfn "After" 
}
 


If you paste this code into Visual Studio, MonoDevelop, or Try F #, you will immediately receive a warning:



warning FS0020: This expression should have type unit, but has type Async ‹unit›. Use ignore to discard the result of the expression, or let to bind the result to a name.


warning FS0020: This expression must be of type unit but is of type Async ‹unit›. Use ignore to discard the result of an expression, or let to associate the result with a name.




You can still compile the code and run it, but if you read the warning you will see that the expression returns Async and you need to wait for its result using do !:



let handler() = async 
{
   printfn "Before"
   do! Async.Sleep(1000)
   printfn "After" 
}
 


Pitfall # 3: Asynchronous Methods That Return Void



Quite a lot of the conversation was devoted to asynchronous void methods. If you write async void Foo () {…}, then the C # compiler generates a method that returns void. But under the hood, it creates and runs a task. This means that you cannot predict when the work will actually be done.



In the speech, the following recommendation was made on using the async void pattern:



image

(For heaven's sake, stop using async void!)



In fairness, it should be noted that asynchronous void methods canbe useful when writing event handlers. Event handlers must return void, and they often start some work that continues in the background. But I don't think this is really useful in the MVVM world (although it certainly does good demos at conferences).



Let me demonstrate the problem with a snippet from the MSDN Magazine article on Asynchronous Programming in C #:



async void ThrowExceptionAsync() 
{
    throw new InvalidOperationException();
}

public void CallThrowExceptionAsync() 
{
    try 
    {
        ThrowExceptionAsync();
    } 
    catch (Exception) 
    {
        Console.WriteLine("Failed");
    }
}
 


Do you think this code will print "Failed"? I hope you already understand the style of this article ...

Indeed, the exception will not be handled, because after starting the job, ThrowExceptionAsync will immediately exit, and the exception will be thrown somewhere in a background thread.



Corresponding F # code



So if you don't need to use the features of a programming language, then it's probably best not to include that feature in the first place. F # does not allow you to write async void functions - if you wrap the body of a function in an async {…} block, the return type will be Async. If you use type annotations and require a unit, you get a type mismatch.



You can write code that matches the above C # code using Async.Start:



let throwExceptionAsync() = async {
    raise <| new InvalidOperationException()  }

let callThrowExceptionAsync() = 
  try
     throwExceptionAsync()
     |> Async.Start
   with e ->
     printfn "Failed"


The exception will not be handled here either. But what's going on is more obvious, because we have to write Async.Start explicitly. If we don't, we get a warning that the function is returning Async and we are ignoring the result (just like in the previous section "Ignoring Results").



Pitfall # 4: Asynchronous lambda functions that return void



The situation becomes even more complicated when you pass an asynchronous lambda function to a method as a delegate. In this case, the C # compiler infers the type of the method from the type of the delegate. If you use an Action delegate (or similar), the compiler creates an asynchronous void function that starts the job and returns void. If you use the Func delegate, the compiler generates a function that returns Task.



Here is a sample from Lucian's slides. When will the next (perfectly correct) code finish - one second (after all tasks have finished waiting) or immediately?



Parallel.For(0, 10, async i => 
{
    await Task.Delay(1000);
});


You won't be able to answer this question unless you know that there are only overloads for For that accept Action delegates - and thus the lambda will always compile as an async void. It also means that adding some (possibly payload) load will be a breaking change.



Corresponding F # code



F # has no special "asynchronous lambda functions", but you can write a lambda function that returns asynchronous computations. Such a function will return Async, so it cannot be passed as an argument to methods that expect a void-returning delegate. The following code won't compile:



Parallel.For(0, 10, fun i -> async {
  do! Async.Sleep(1000) 
})


The error message simply says that the function type int -> Async is not compatible with the Action delegate (in F # it should be int -> unit):



error FS0041: No overloads match for method For. The available overloads are shown below (or in the Error List window).


error FS0041: No overloads found for the For method. The available overloads are shown below (or in the error list box).




To get the same behavior as in the above C # code, we must explicitly start. If you want to run an asynchronous sequence in the background, this is easily done with Async.Start (which takes an asynchronous computation that returns a unit, schedules it, and returns a unit):



Parallel.For(0, 10, fun i -> Async.Start(async {
  do! Async.Sleep(1000) 
}))


You can of course write this, but it's pretty easy to see what's going on. It is also easy to see that we are wasting resources, as the peculiarity of Parallel.For is that it performs CPU intensive computations (which are usually synchronous functions) in parallel.



Pitfall # 5: Nesting tasks



I think Lucian included this stone just to test the intelligence of the people in the audience, but here it is. The question is, will the following code wait 1 second between two pins to the console?



Console.WriteLine("Before");
await Task.Factory.StartNew(
    async () => { await Task.Delay(1000); });
Console.WriteLine("After");


Quite unexpectedly, there is no delay between these conclusions. How is this possible? The StartNew method takes a delegate and returns Task where T is the type returned by the delegate. In our case, the delegate returns Task, so we get Task as a result. await only waits for the outer task to complete (which immediately returns the inner task), while the inner task is ignored.



In C #, this can be fixed by using Task.Run instead of StartNew (or by removing async / await in the lambda function).



Can you write something like this in F #? We can create a task that will return Async using the Task.Factory.StartNew function and a lambda function that returns an async block. To wait for the task to complete, we will need to convert it to asynchronous execution using Async.AwaitTask. This means that we get Async <Async>:



async {
  do! Task.Factory.StartNew(fun () -> async { 
    do! Async.Sleep(1000) }) |> Async.AwaitTask }


Again, this code doesn't compile. The problem is that the do! requires Async on the right, but actually receives Async <Async>. In other words, we cannot just ignore the result. We need to do something about this explicitly

(you can use Async.Ignore to reproduce the C # behavior). The error message may not be as clear as the previous ones, but it gives a general idea:



error FS0001: This expression was expected to have type Async ‹unit› but here has type unit


error FS0001: Async expression 'unit' expected, unit type present


Pitfall # 6: Async doesn't work



Here's another problematic piece of code from Lucian's slide. This time, the problem is pretty simple. The following snippet defines an asynchronous FooAsync method and calls it from the Handler, but the code does not execute asynchronously:



async Task FooAsync() 
{
    await Task.Delay(1000);
}
void Handler() 
{
    FooAsync().Wait();
}


It's easy to spot the problem - we call FooAsync (). Wait (). This means that we create a task and then, using Wait, we block the program until it completes. A simple removal of Wait solves the problem, because we just want to start the task.



You can write the same code in F #, but asynchronous workflows do not use .NET tasks (originally designed for CPU bound computation), but instead use the F # Async type, which is not bundled with Wait. This means that you must write:



let fooAsync() = async {
    do! Async.Sleep(1000) }
let handler() = 
    fooAsync() |> Async.RunSynchronously


Of course, such code can be written by accident, but if you are faced with a problem of broken asynchrony , you will easily notice that the code calls RunSynchronously, so the work is done - as the name suggests - synchronously .



Summary



In this article, I've looked at six cases in which the asynchronous programming model in C # behaves in unexpected ways. Most of them are based on Lucian and Stephen's conversation at the MVP Summit, so thanks to both of them for an interesting list of common pitfalls!



For F #, I tried to find the closest relevant code snippets using asynchronous workflows. In most cases, the F # compiler will issue a warning or error — or the programming model has no (direct) way to express the same code. I think this confirms a statement I made in a previous blog post : “The F # programming model definitely seems more appropriate for functional (declarative) programming languages. I also think it makes it easier to reason about what's going on. "



Finally, this article should not be understood as a destructive criticism of asynchrony in C # :-). I fully understand why the design of C # follows the same principles that it follows - for C # it makes sense to use Task (instead of separate Async), which has a number of consequences. And I can understand the reasons for the other decisions - this is probably the best way to integrate asynchronous programming in C #. But at the same time, I think F # does a better job - partly because of its compositing ability, but more importantly because of cool add-ons like F # agents . In addition, asynchrony in F # also has its problems (the most common mistake is that tail recursive functions should be used return! Instead of do !, to avoid leaks), but this is a topic for a separate blog post.



PS From the translator. The article was written in 2013, but I found it interesting and relevant enough to be translated into Russian. This is my first post on Habré, so don't kick hard.



All Articles