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 [5]. 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 [1] 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 has first 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: async Task WorkThenWait() { 2: Thread.Sleep(1000); 3: Console.WriteLine("work"); 4: await Task.Delay(1000); 5: } 6: 7: void Demo() { 8: var child = WorkThenWait(); 9: Console.WriteLine("started"); 10: child.Wait(); 11: Console.WriteLine("completed"); 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: let workThenWait() = 2: Thread.Sleep(1000) 3: printfn "work done" 4: async { do! Async.Sleep(1000) } 5: 6: let demo() = 7: let work = workThenWait() |> Async.StartAsTask 8: printfn "started" 9: work.Wait() 10: 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: let workThenWait() = async { 2: Thread.Sleep(1000) 3: printfn "work done" 4: 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: async Task Handler() { 2: Console.WriteLine("Before"); 3: Task.Delay(1000); 4: Console.WriteLine("After"); 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: let handler() = async { 2: printfn "Before" 3: Async.Sleep(1000) 4: 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 typeunit
, 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: let handler() = async { 2: printfn "Before" 3: do! Async.Sleep(1000) 4: 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 [2] on asynchronous programming in C#:
1: async void ThrowExceptionAsync() { 2: throw new InvalidOperationException(); 3: } 4: 5: public void CallThrowExceptionAsync() { 6: try { 7: ThrowExceptionAsync(); 8: } catch (Exception) { 9: Console.WriteLine("Failed"); 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: let throwExceptionAsync() = async { 2: raise <| new InvalidOperationException() } 3: 4: let callThrowExceptionAsync() = 5: try 6: throwExceptionAsync() 7: |> Async.Start 8: with e -> 9: 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 returnsAsync<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: Parallel.For(0, 10, async i => { 2: await Task.Delay(1000); 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: Parallel.For(0, 10, fun i -> async { 2: do! Async.Sleep(1000) 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 methodFor
. 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: Parallel.For(0, 10, fun i -> Async.Start(async { 2: do! Async.Sleep(1000) 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: Console.WriteLine("Before"); 2: await Task.Factory.StartNew( 3: async () => { await Task.Delay(1000); }); 4: 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>
usingTask.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: async { 2: do! Task.Factory.StartNew(fun () -> async { 3: 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 typeAsync<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: async Task FooAsync() { 2: await Task.Delay(1000); 3: } 4: void Handler() { 5: FooAsync().Wait(); 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: let fooAsync() = async { 2: do! Async.Sleep(1000) } 3: let handler() = 4: 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 [4] 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 separateAsync<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 [3]. Also, F# async has its problems too (the most common gotcha is that tail-recursive functions must usereturn!
instead of do!
to avoid leaks), but that is a topic for a separate blog post.References
- [1] Async Clinic - Lucian Wischik, Stephen Toub
- [2] Best Practices in Asynchronous Programming - Stephen Cleary
- [3] An Introduction To F# Agents - Tomas Petricek
- [4] Asynchronous C# and F# (I.): Simultaneous introduction - Tomas Petricek
- [5] How F# Learned to Stop Worrying and Love the Data - Tomas Petricek (Channel 9)
Published: April 15, 2013 04:00
Tags: C# language | Functional Programming in .NET | F# language | Asynchronous
Tags: C# language | Functional Programming in .NET | F# language | Asynchronous
- RE: Async in C# and F#: Asynchronous gotchas in C# by Stephen Cleary (4/15/2013 6:14:26 PM)Excellent article!
A few comments:
#1: My background is C++, where I've had to do some uncomfortable CPS in the past. I'm not sure how F# async workflows behave, but if you view async/await as a compiler transformation to CPS, then starting the method synchronously is natural. So for me, Gotcha #1 is actually expected behavior.
#2: The C# compiler also gives a warning.
#3 & #4: Both results of allowing async void.
I've heard that the async/await team wanted to disallow "async void", which is certainly more correct from a language design perspective. I expect the usefulness of async event handlers was just too much, though. (Of course, if C#/CLR could afford a redesign, it would be better to overhaul their entire "event" system which feels like something out of the '90s... Look! It's RAD!).
Anyway, F# has the advantages of a smaller and more advanced user base, so they can afford to take the more correct route. :)
#5 & #6: Both results of reusing the Task type instead of using a new type.
I'm sure this was another tough-to-call decision. There's a lot of benefits to reusing the Task type (many of us in the community were writing Task-based asynchronous APIs for a couple of years before async/await), but then you get a bunch of members that "must be used with care" and there's no MSDN docs pointing this out.
Another one that falls into this category is the Task constructor. This is a pretty common error for developers who have never used the TPL: when they need to return a Task they call the constructor and return it.
And another one similar to #6 is Task.WaitAll and Task.WaitAny, which async newbies commonly confuse with Task.WhenAll and Task.WhenAny. - RE: Async in C# and F#: Asynchronous gotchas in C# by Boris Letocha (4/15/2013 6:22:22 PM)After you will write about F# problems you can mention why this:
open System.Diagnostics;
open System.Threading.Tasks;
let DoSomething = async { () }
let DoSomethingRepeately = async { for i in 1 .. 10000000 do do! DoSomething }
[<EntryPoint>]
let main argv =
DoSomethingRepeately |> Async.RunSynchronously
let sw = Stopwatch.StartNew()
DoSomethingRepeately |> Async.RunSynchronously
sw.Stop()
printfn "%A" sw.ElapsedMilliseconds
0 // return an integer exit code
Is over 4 times slower than this C#:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
class Program
{
async Task DoSomething()
{
await Task.Delay(0);
}
async Task DoSomethingRepeately()
{
for (var i = 0; i < 10000000; i++)
{
await DoSomething();
}
}
static void Main(string[] args)
{
new Program().DoSomethingRepeately().Wait();
var sw = Stopwatch.StartNew();
new Program().DoSomethingRepeately().Wait();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
}
}
I think this shows that Gotcha #1 is actually great feature and C# is ahead of F# in async composability. - RE: Async in C# and F#: Asynchronous gotchas in C# by Boris Letocha (4/15/2013 6:34:48 PM)Sorry for formatting, it was not intentional to show that C# is ahead of F# in pasting to blog comments :-D
- RE: Async in C# and F#: Asynchronous gotchas in C# by Tomas (4/15/2013 7:05:35 PM)Thanks for the comments!
@Stephen Cleary - You're right, the C# compiler also gives you warning in the second case. But that is very ad-hoc check. It does not prevent other similar problems - for example, when you have a nested task:
Console.WriteLine("Before");
await Task.Factory.StartNew(() => Task.Delay(1000));
Console.WriteLine("After");
In this case, F# simply benefits from the fact that it is expression-based and declarative. In that setting, ignoring results is always wrong. This problem is not actually specific to C# async - if you google search for "C# gotchas" you find that this makes programming with immutable types in .NET painful elsewhere - see [1].
As for the other comments - as I said in the summary, I fully understand the C# design and I think it is probably the best that the C# designers could do (reuse tasks, make it fit with the C# model...). But I still think that writing asynchronous code in F# is a better idea, simply because the declarative nature of F# makes it easier to avoid the pitfalls.
It is good to know that #1 is the expected behavior, at least for someone :-). But to me, the fact that the method returns Task suggests that it does not block. (And I think it is nice to be able to specify this scope explicitly with a code block....)
@Boris Letocha - Your comparison is not relevant, because it is comparing the performance of code snippets that do not anything - and do not model any realistic setting. The F# async model is designed for I/O bound computations and so a certain overhead of async workflows is not a problem. If you are writing CPU-intensive parallel code, then you would use Tasks in F# too. (But see the series I wrote earlier for more details [2]).
[1] http://stackoverflow.com/questions/241134/what-is-the-worst-gotcha-in-c-sharp-or-net
[1] http://tomasp.net/blog/csharp-fsharp-async-intro.aspx - RE: Async in C# and F#: Asynchronous gotchas in C# by Boris Letocha (4/15/2013 7:26:56 PM)Of course it was just simplification. You can think that DoSomething is async getter from some cache which is mostly hit and then it is very relevant.
- RE: Async in C# and F#: Asynchronous gotchas in C# by Tom (4/15/2013 10:51:04 PM)#3: Async void methods
"the exception is not handled because ThrowExceptionAsync starts the work and returns immediately (and the exception happens somewhere on a background thread)."
This is not quite true. The method runs synchronously, so the exception is thrown synchronously too, but it is wrapped by a Task object. However, this Task cannot be accessed because the return type is void.
If no await keyword used in an async method execution flow, then the method finishes synchronously. - RE: Async in C# and F#: Asynchronous gotchas in C# by Petr Onderka (4/15/2013 11:26:40 PM)In addition to what others said:
Gotcha #3 was surprising to me, but that's because the async method won't run on a background thread, since there is no await. But you're right that the exception can't be caught like that.
Also, in gotcha #5, dropping async and await (i.e. changing the line to "await Task.Factory.StartNew(() => Task.Delay(1000))" won't actually help, because the result is still Task<Task>. - RE: Async in C# and F#: Asynchronous gotchas in C# by Vasily (4/16/2013 1:54:44 AM)Great article, thank you.
- RE: Async in C# and F#: Asynchronous gotchas in C# by voyance gratuite par mail (5/14/2013 1:38:39 PM)Everything is well designed your site and very nice with many choices, it is a wonder! Congratulations. friendly
No comments:
Post a Comment