Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Async.AwaitTask work in f#?

Tags:

f#

I understand that the main difference between f# and c# async model is that in f#, the async execution does not begin unless you call something like Async.RunSynchronously. In c#, when a method returns a task, execution is usually (not always) immediately started in a background thread.

Async.AwaitTask documentation says "Returns an asynchronous computation that waits for the given task to complete and returns its result."

Does that mean, when you call a c# method that returns a task, execution already started in the background? If so, what is the point of wrapping it inside an Async type anymore?

like image 269
user3587180 Avatar asked Jul 05 '18 01:07

user3587180


1 Answers

The point of wrapping a Task inside an Async is to more easily compose it with other asyncs, or to use it with let! from inside an async { ... } block. And in the latter case, the wrapped Task won't be started until its enclosing async { ... } block is started.

For example, let's look at the following function:

let printTask str =
  async {
    printfn "%s" str
  } |> Async.StartAsTask

This doesn't do much; its only reason for existence is so that you can tell when it has started running, because it will print a message to the screen. If you call it from F# Interactive:

printTask "Hello"

You'll see the following output:

Hello
val it : Threading.Tasks.Task<unit> =
  System.Threading.Tasks.Task`1[Microsoft.FSharp.Core.Unit]
    {AsyncState = null;
     CreationOptions = None;
     Exception = null;
     Id = 4;
     IsCanceled = false;
     IsCompleted = true;
     IsCompletedSuccessfully = true;
     IsFaulted = false;
     Status = RanToCompletion;}

So it printed "Hello" and then returned the completed Task. This proves that the Task was started immediately.

But now look at the following code:

open System.Net
open System
open System.IO

let printTask str =
  async {
    printfn "%s" str
  } |> Async.StartAsTask

let fetchUrlAsync url =
  async {
    let req = WebRequest.Create(Uri(url))
    do! printTask ("started downloading " + url) |> Async.AwaitTask
    use! resp = req.GetResponseAsync() |> Async.AwaitTask
    use stream = resp.GetResponseStream()
    use reader = new IO.StreamReader(stream)
    let html = reader.ReadToEnd()
    do! printTask ("finished downloading " + url) |> Async.AwaitTask
  }

(This is Scott Wlaschin's "Async Web Downloader" example, adapted to use Tasks instead of Asyncs internally).

Here, the async { ... } block contains three Tasks, all of which are wrapped in Async.AwaitTask. (Note that if you removed |> Async.AwaitTask from any of these lines, you'd get a type error). For each Task, once its line of code executes, it will start right away. But that's an important point, because the overall async { ... } computation is not started right away. So I can do:

let a = fetchUrlAsync "http://www.google.com"

And the only thing printed in F# Interactive is val a : Async<unit>. I can wait as long as I want to, and nothing else is printed. Only once I actually start a will it start running:

a |> Async.RunSynchronously

This prints started downloading http://www.google.com immediately, then after a short pause it prints finished downloading http://www.google.com.

So that's the purpose of Async.AwaitTask: to allow async { ... } blocks to more easily interoperate with C# code that returns Tasks. And if the Async.AwaitTask call is inside an async { ... } block, then the Task won't actually start until the enclosing Async is started, so you still get all the advantages of "cold-start" Asyncs.

like image 148
rmunn Avatar answered Sep 28 '22 06:09

rmunn