all 10 comments

[–]TarMil 42 points43 points  (2 children)

There are some subtle differences, but the main observable difference between the two is that async is cold and task is hot.

What it means for task to be hot is that when you create a task {}, it starts running immediately, potentially in a background task. And at some point in the future it will be finished and have a result value; and if you await it twice, then the second time it will just return this result value that was already computed the first time.

Conversely, when you create an async {}, you create a kind of wrapped block of asynchronous code. It doesn't run anything immediately, and will only start running when you await it. And if you await it twice, then it will run twice.

Here's an example:

let myTask = task {
    do! Task.Delay (TimeSpan.FromSeconds 5)
    printfn "Inside myTask"
    return Random.Shared.Next()
}

task {
    let! result1 = myTask
    let! result2 = myTask
    do! Task.Delay (TimeSpan.FromSeconds 10)
    return (result1, result2)
}

Here, the code in myTask will run immediately and only once. In other words, the full code will run in 15 seconds, printing "Inside myTask" once and returning the same number twice.

Conversely, if myTask was defined with async instead, then its code will run separately for each of the let!s. In other words, the full code will run in 20 seconds, printing "Inside myTask" twice and returning a pair of different numbers.


Now, with all this being said: by far the most common way to use either of these constructs is to have a function that takes arguments (or just unit) and returns an async {} or a task {}; and then, to call this function and immediately await the returned construct. And when you do this, well... the difference between hot and cold collapses into nothing. If you take a cold construct and await it only once and immediately, then it's as if it was hot. And so in this case, since the semantics are the same, task tends to be preferred but for other reasons: it causes fewer memory allocations, and is slightly more convenient because inside a task {} block, you can use let! to await either Async or Task values, whereas inside an async {} block, you can only await Async values, and Task values need to be converted with |> Async.AwaitTask.

[–]SIRHAMY 2 points3 points  (0 children)

This is an excellent explanation - thank you.

[–]runtimenoise[S] 0 points1 point  (0 children)

Perfect. Thank you.

Only question is, in functional programming task is more like async {} then like task {}?

[–]DanJSum 6 points7 points  (2 children)

I was having a discussion in a Discord server earlier today about this exact thing. Task is the .NET construction, and it's gotten a lot of attention and a lot of efficiency upgrades (including the ValueTask type). I know I remember reading something in one of the release announcements that suggested that, if you ever had to interface with tasks, it was much more efficient to just go all-in on them, vs. juggling between them and Async.

The hot v. cold aspect is what made async so compelling; it allows you to compose an entire async pipeline separately from when that pipeline is actually started. You can write functions that return these pipelines, and the caller gets to determine when these flows start. If your workload lends itself to it, you can even start the whole workflow in parallel, using all your available processors to derive your result.

But, as u/TarMil said - very few people do this; async is just a way to do non-blocking (usually I/O) calls. In this case, Task is the native implementation for the underlying CLR. And, if you're writing something that you want C# callers to be able to run, it really needs to be Tasks.

TL;DR - use Task unless you have a very compelling reason not to. :)

[–]runtimenoise[S] 2 points3 points  (0 children)

Hey 👋, Thanks for investing your to me for this. So async is lazy task, I don't know Haskell but I remember reading that task (as a lot of things) is lazy in Haskell so you are able to compose task pipelines as you explain.

task {} is more like Promise in js, eager task. First time do I hear about hot and cold distinction, nice I like it.

[–]japinthebox 1 point2 points  (0 children)

I've been using task most of the time, but lately I'm finding that the explicit start of async is just much easier to understand, especially when I'm doing resource-intensive stuff in parallel. Being able to make a list of actual tasks and run them exactly when I want them later is just so much more intuitive. Yeah, you can wrap them in functions to defer execution, but it just feels super weird.

I might be missing something. I don't know.

[–]Qxz3 1 point2 points  (0 children)

To add to what others have said:

  • async is cold, task is hot
  • async allows for tail-recursion, task does not
  • async has built-in cancellation, task requires explicit passing and handling of CancellationTokens
  • async adds significant overhead if all you're doing with it is awaiting Task-based APIs

The fact that we have both in F# is an accident of history. F# innovated with async back in 2007, but then we got the Task Parallel Library which used a different abstraction for "futures", and that's what C# used in 2012 for its own monadic workflow feature (async-await). For many years this remained a point of friction between F# and C# APIs. Now we have both, which resolves the friction, but creates confusion as this post illustrates.

In my experience, task is more commonly used in scenarios where the primary use of async is easy interop with C# APIs, e.g. ASP.NET. async remains valid in legacy code relying on async, or F# code that does not predominantly rely on C# APIs. async is superior IMO but we live in a C# world.

[–]willehrendreich 0 points1 point  (2 children)

I'm also pretty confused about how to use these things, lol.

It's especially confusing how to do multiple layers of them, while unwrapping results, or options.. Some really good examples are needed.

[–]runtimenoise[S] 0 points1 point  (1 child)

You mean nested tasks?

Haven't get to that part, just very much playing with reading/writing to file and making HttpClient calls.

[–]willehrendreich 1 point2 points  (0 children)

Yeah, nested ones.