TP

Async in C# and F# Asynchronous gotchas in C#

Back in February, I attended the annual MVP summit - an event organized by Microsoft for MVPs. I used that opportunity to also visit Boston and New York and do two F# talks and to record a Channel9 lecutre about type providers. Despite all the other activities (often involving pubs, other F# people and long sleeping in the mornings), I also managed to come to some talks!

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

Why is that? It turns out that many of the common errors are not possible (or much less likely) when using the F# asynchronous model (which has been around since F# 1.9.2.7, which was released in 2007 and have been shipped with Visual Studio 2008).

Gotcha #1: Async does not run asynchronously

Let's go straight to the first tricky aspect of the C# asynchronous programming model. Take a look at the following example and figure out in what order will the strings be printed (I could not find the exact code shown at the talk, but I remember Lucian showing something similar):

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
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 guessed that it prints "started", "work" and "completed" then you're wrong. The code prints "work", "started" and "completed", try it! What the author intended was to start the work (by calling WorkThenWait) and then await for the task later. The problem is that WorkThenWait starts by doing some heavy computations (here, Thread.Sleep) and only after that uses await.

In C#, the first part of the code in async method is executed synchronously (on the thread of the caller). You could fix that, for example, by adding await Task.Yield() at the beginning.

Corresponding F# code

This is not a problem in F#. When writing async code in F#, the entire code inside async { ... } block is all delayed and only started later (when you explicitly start it). The above C# code corresponds to the following F#:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
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"

It is quite clear that the workThenWait function is not doing the work (Thread.Sleep) as part of the asynchronous computation and that it will be executed when the function is called (and not when the async workflow is started). The usual F# pattern is to wrap the entire function body in async. In F#, you would write the following, which works as expected:

1: 
2: 
3: 
4: 
let workThenWait() = async { 
  Thread.Sleep(1000)
  printfn "work done"
  do! Async.Sleep(1000) }

Gotcha #2: Ignoring results

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

1: 
2: 
3: 
4: 
5: 
async Task Handler() {
  Console.WriteLine("Before");
  Task.Delay(1000);
  Console.WriteLine("After");
}

Were you expecting that it prints "Before", waits 1 second and then prints "After"? Wrong! It prints both messages immediately without any waiting in between. The problem is that Task.Delay returns a Task and we forgot to await until it completes using await.

Corresponding F# code

Again, you would probably not hit this issue in F#. You can surely write code that calls Async.Sleep and ignores the returned Async<unit>:

1: 
2: 
3: 
4: 
let handler() = async {
  printfn "Before"
  Async.Sleep(1000)
  printfn "After" }

If you paste the code in Visual Studio, MonoDevelop or Try F#, you get an immediate feedback with a warning saying that:

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.

You can still compile the code and run it, but if you read the warning, you'll see that the expression returns Async<unit> and you need to await it using do!:

1: 
2: 
3: 
4: 
let handler() = async {
  printfn "Before"
  do! Async.Sleep(1000)
  printfn "After" }

Gotcha #3: Async void methods

Quite a lot of time in the talk was dedicated to async void methods. If you write async void Foo() { ... }, then the C# compiler generates a method that returns void. Under the cover, it creates and starts a task. This means that you have no way of telling when the work has actually happened.

Here is a recommendation on the async void pattern from the talk:

To be fair - async void methods can be useful when you're writing an event handler. Event handlers should return void and they often start some work that continues in background. But I do not think this is really useful in the world of MVVM - but it surely makes nice demos at conference talks.

Let me demonstrate the problem using a snippet from MSDN Magazine article on asynchronous programming in C#:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
async void ThrowExceptionAsync() {
  throw new InvalidOperationException();
}

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

Do you think that the code prints "Failed"? I suppose you already understood the style of this blog post... Indeed, the exception is not handled because ThrowExceptionAsync starts the work and returns immediately (and the exception happens somewhere on a background thread).

Corresponding F# code

So, if you should not be using a programming language feature, then it is probably better not to include the feature in the first place. F# does not let you write async void functions - when you wrap function body in the async { ... } block, its return type will be Async<T>. If you used type annotations and demanded unit, you would get a type mismatch.

You can still write code that corresponds to the above C# using Async.Start:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let throwExceptionAsync() = async {
  raise <| new InvalidOperationException() }

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

This will also not handle the exception. But it is more obvious what is going on because we had to write Async.Start explicitly. If we did not write it, we would get a warning saying that the function returns Async<void> and we are ignoring the result (the same as in the earlier section "Ignoring results").

Gotcha #4: Async void lambda functions

Even trickier case is when you pass asynchronous lambda function to some method as a delegate. In this case, the C# compiler infers the type of method from the delegate type. If you use the Action delegate (or similar), then the compiler produces async void function (which starts the work and returns void). If you use the Func<Task> delegate, the compiler generates a function that returns Task.

Here is a sample from Lucian's slides. Does the following (perfectly valid) code finish in 1 second (after all the tasks finish sleeping), or does it finish immediately?

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

You cannot know that, unless you know that For only has overloads that take Action delegates - and thus the lambda function will always be compiled as async void. This also means that adding such (maybe useful?) overload would be a breaking change.

Corresponding F# code

The F# language does not have special "async lambda functions", but you can surely write a lambda function that returns asynchronous computation. The return type of such function will be Async<T> and so it cannot be passed as an argument to methods that expect void-returning delegate. The following F# code does not compile:

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

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

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

To get the same behaviour as the above C# code, we need to explicitly start the work. If you want to start asynchronous workflow in the background, then you can easily do that using Async.Start (which takes a unit-returning asynchronous computation, schedules it and returns unit):

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

You can certainly write this, but it is quite easy to see what is going on. It is also not difficult to see that we are wasting resources, because the point of Parallel.For is that it runs CPU-intensive computations (which are typically synchronous functions) in parallel.

Gotcha #5: Nesting of tasks

I think that Lucian included the next one just to test the mental-compilation skills of the people in the audience, but here it is. The question is, does the following code wait 1 second between the two prints?

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

Again, quite unexpectedly, this does not actually wait between the two writes. How is that possible? The StartNew method takes a delegate and returns a Task<T> where T is the type returned by the delegate. In the above case, the delegate returns Task, so we get Task<Task> as the result. Using await waits only for the completion of the outer task (which immediately returns the inner task) and the inner task is then ignored.

In C#, you can fix this by using Task.Run instead of StartNew (or by dropping the async and await in the lambda function).

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

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

Again, this code does not compile. The problem is that the do! keyword requires Async<unit> on the right-hand side, but it actually gets Async<Async<unit>>. In other words, we cannot simply ignore the result. We need to explicitly do something with it (we could use Async.Ignore to replicate the C# behaviour). The error message might not be as clear as the earlier messages, but you can get the idea:

error FS0001: This expression was expected to have type Async<unit> but here has type unit

Gotcha #6: Not running asynchronously

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

1: 
2: 
3: 
4: 
5: 
6: 
async Task FooAsync() {
  await Task.Delay(1000);
}
void Handler() {
  FooAsync().Wait();
}

It is not too difficult to spot the issue - we are calling FooAsync().Wait(). This means that we create a task and then, using Wait, block until it completes. Simply removing Wait fixes 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 (which were originally designed for CPU-bound computations) and instead uses F# Async<T> which does not come with Wait. This means you have to write:

1: 
2: 
3: 
4: 
let fooAsync() = async {
  do! Async.Sleep(1000) }
let handler() = 
  fooAsync() |> Async.RunSynchronously

You could certainly write such code by accident, but if you face a problem that it does not run asynchronously, you can easily spot that the code calls RunSynchronously and so the work is done - as the name suggests - synchronously.

Summary

In this article, I looked at six cases where the C# asynchronous programming model behaves in an unexpected way. Most of them were based on a talk by Lucian and Stephen at the MVP summit, so thanks to both of them for sharing an interesting list of common pitfalls!

I tried to find the closest corresponding code snippet in F#, using asynchronous workflows. In most of the cases, the F# compiler reports a warning or an error - or the programming model does not have a (direct) way to express the same code. I think this supports the claim that I made in an earlier blog post that "The F# programming model definitely feels more suitable for functional (declarative) programming languages. I also think that it makes it easier to reason about what is going on".

Finally, this article should not be understood as a devastating criticism of C# async :-). I can fully understand why the C# design follows the principles it follows - for C#, it makes sense to use Task<T> (instead of separate Async<T>), which has a number of implications. And I can understand the reasoning behind other decisions too - it is likely the best way to integrate asynchronous programming in C#. But at the same time, I think F# does a better job - partly because of the composability, but more importantly because of greate additions like the F# agents. Also, F# async has its problems too (the most common gotcha is that tail-recursive functions must use return! instead of do! to avoid leaks), but that is a topic for a separate blog post.

namespace System
namespace System.Threading
namespace System.Threading.Tasks
val workThenWait : unit -> Async<unit>
Multiple items
type Thread =
  inherit CriticalFinalizerObject
  new : start:ThreadStart -> Thread + 3 overloads
  member Abort : unit -> unit + 1 overload
  member ApartmentState : ApartmentState with get, set
  member CurrentCulture : CultureInfo with get, set
  member CurrentUICulture : CultureInfo with get, set
  member DisableComObjectEagerCleanup : unit -> unit
  member ExecutionContext : ExecutionContext
  member GetApartmentState : unit -> ApartmentState
  member GetCompressedStack : unit -> CompressedStack
  member GetHashCode : unit -> int
  ...

--------------------
Thread(start: ThreadStart) : Thread
Thread(start: ParameterizedThreadStart) : Thread
Thread(start: ThreadStart, maxStackSize: int) : Thread
Thread(start: ParameterizedThreadStart, maxStackSize: int) : Thread
Thread.Sleep(timeout: TimeSpan) : unit
Thread.Sleep(millisecondsTimeout: int) : unit
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
val async : AsyncBuilder
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
static member Async.Sleep : millisecondsDueTime:int -> Async<unit>
val demo : unit -> unit
val work : Task<unit>
static member Async.StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
Task.Wait() : unit
Task.Wait(millisecondsTimeout: int) : bool
Task.Wait(cancellationToken: CancellationToken) : unit
Task.Wait(timeout: TimeSpan) : bool
Task.Wait(millisecondsTimeout: int, cancellationToken: CancellationToken) : bool
val handler : unit -> Async<unit>
val throwExceptionAsync : unit -> Async<unit>
val raise : exn:Exception -> 'T
Multiple items
type InvalidOperationException =
  inherit SystemException
  new : unit -> InvalidOperationException + 2 overloads

--------------------
InvalidOperationException() : InvalidOperationException
InvalidOperationException(message: string) : InvalidOperationException
InvalidOperationException(message: string, innerException: exn) : InvalidOperationException
val callThrowExceptionAsync : unit -> unit
static member Async.Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
val e : exn
type Parallel =
  static member For : fromInclusive:int * toExclusive:int * body:Action<int> -> ParallelLoopResult + 11 overloads
  static member ForEach<'TSource> : source:IEnumerable<'TSource> * body:Action<'TSource> -> ParallelLoopResult + 19 overloads
  static member Invoke : [<ParamArray>] actions:Action[] -> unit + 1 overload
Parallel.For(fromInclusive: int64, toExclusive: int64, body: Action<int64,ParallelLoopState>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int, toExclusive: int, body: Action<int,ParallelLoopState>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int64, toExclusive: int64, body: Action<int64>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int, toExclusive: int, body: Action<int>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int64, toExclusive: int64, parallelOptions: ParallelOptions, body: Action<int64,ParallelLoopState>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int, toExclusive: int, parallelOptions: ParallelOptions, body: Action<int,ParallelLoopState>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int64, toExclusive: int64, parallelOptions: ParallelOptions, body: Action<int64>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For(fromInclusive: int, toExclusive: int, parallelOptions: ParallelOptions, body: Action<int>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For<'TLocal>(fromInclusive: int64, toExclusive: int64, localInit: Func<'TLocal>, body: Func<int64,ParallelLoopState,'TLocal,'TLocal>, localFinally: Action<'TLocal>) : ParallelLoopResult
   (+0 other overloads)
Parallel.For<'TLocal>(fromInclusive: int, toExclusive: int, localInit: Func<'TLocal>, body: Func<int,ParallelLoopState,'TLocal,'TLocal>, localFinally: Action<'TLocal>) : ParallelLoopResult
   (+0 other overloads)
val i : 'a
val i : int
Multiple items
type Task =
  new : action:Action -> Task + 7 overloads
  member AsyncState : obj
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable
  member ContinueWith : continuationAction:Action<Task> -> Task + 19 overloads
  member CreationOptions : TaskCreationOptions
  member Dispose : unit -> unit
  member Exception : AggregateException
  member GetAwaiter : unit -> TaskAwaiter
  member Id : int
  member IsCanceled : bool
  ...

--------------------
type Task<'TResult> =
  inherit Task
  new : function:Func<'TResult> -> Task<'TResult> + 7 overloads
  member ConfigureAwait : continueOnCapturedContext:bool -> ConfiguredTaskAwaitable<'TResult>
  member ContinueWith : continuationAction:Action<Task<'TResult>> -> Task + 19 overloads
  member GetAwaiter : unit -> TaskAwaiter<'TResult>
  member Result : 'TResult
  static member Factory : TaskFactory<'TResult>

--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(function: Func<'TResult>) : Task<'TResult>
Task(function: Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
Task(function: Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(function: Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(function: Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Multiple items
property Task.Factory: TaskFactory<'TResult>

--------------------
property Task.Factory: TaskFactory
Multiple items
TaskFactory.StartNew(function: Func<'TResult>) : Task<'TResult>
TaskFactory.StartNew(function: Func<obj,'TResult>, state: obj) : Task<'TResult>
TaskFactory.StartNew(function: Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
TaskFactory.StartNew(function: Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
TaskFactory.StartNew(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
TaskFactory.StartNew(function: Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
TaskFactory.StartNew(function: Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions, scheduler: TaskScheduler) : Task<'TResult>
TaskFactory.StartNew(function: Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions, scheduler: TaskScheduler) : Task<'TResult>

--------------------
TaskFactory.StartNew<'TResult>(function: Func<'TResult>) : Task<'TResult>
   (+0 other overloads)
TaskFactory.StartNew(action: Action) : Task
   (+0 other overloads)
TaskFactory.StartNew<'TResult>(function: Func<obj,'TResult>, state: obj) : Task<'TResult>
   (+0 other overloads)
TaskFactory.StartNew<'TResult>(function: Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
   (+0 other overloads)
TaskFactory.StartNew<'TResult>(function: Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
   (+0 other overloads)
TaskFactory.StartNew(action: Action<obj>, state: obj) : Task
   (+0 other overloads)
TaskFactory.StartNew(action: Action, creationOptions: TaskCreationOptions) : Task
   (+0 other overloads)
TaskFactory.StartNew(action: Action, cancellationToken: CancellationToken) : Task
   (+0 other overloads)
TaskFactory.StartNew<'TResult>(function: Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
   (+0 other overloads)
TaskFactory.StartNew<'TResult>(function: Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
   (+0 other overloads)
static member Async.AwaitTask : task:Task -> Async<unit>
static member Async.AwaitTask : task:Task<'T> -> Async<'T>
val fooAsync : unit -> Async<unit>
val handler : unit -> unit
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T

Published: Monday, 15 April 2013, 4:00 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: async, c#, f#