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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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 typeAsync<unit>
. Useignore
to discard the result of the expression, orlet
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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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: |
|
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 typeunit
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: |
|
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: |
|
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.
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(millisecondsTimeout: int) : unit
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> =
Task.Wait(millisecondsTimeout: int) : bool
Task.Wait(cancellationToken: CancellationToken) : unit
Task.Wait(timeout: TimeSpan) : bool
Task.Wait(millisecondsTimeout: int, cancellationToken: CancellationToken) : bool
type InvalidOperationException =
inherit SystemException
new : unit -> InvalidOperationException + 2 overloads
--------------------
InvalidOperationException() : InvalidOperationException
InvalidOperationException(message: string) : InvalidOperationException
InvalidOperationException(message: string, innerException: exn) : InvalidOperationException
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
(+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)
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>
property Task.Factory: TaskFactory<'TResult>
--------------------
property Task.Factory: TaskFactory
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<'T> -> Async<'T>
Published: Monday, 15 April 2013, 4:00 AM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: async, c#, f#