Running with Async

Creating an asynchronous workflow doesn’t execute it immediately. It’s up to us to determine when and how we want the workflow to execute. So far we’ve been using Async.RunSynchronously to run our asynchronous workflow. By doing so, we are saying execute the async and wait for the result. This may not always be what we need. What if we want to write to a log file without blocking the current execution? We could use Async.Start.

Let’s revisit some code we looked at earlier. (Asyncing the F# Way)

let waiting () =
    async {
        printfn "Good night"
        do! Async.Sleep 2000
        printfn "Good morning"
    } |> Async.RunSynchronously
    printfn "Hello"

We get the expected results of:

Good night
Good morning
Hello

If we make a small change…

let nowaiting () =
    async {
        printfn "Good night"
        do! Async.Sleep 2000
        printfn "Good morning"
    } |> Async.Start
    printfn "Hello"

We’ve change the Async.RunSynchronously to Async.Start and our results are much different. We end up with results like this:

Hello
Good night
Good morning

(You’re results may be slightly different, however the “Hello” should always come before “Good morning”)

The reason Hello should always come before Good morning is we are no longer waiting for the result of the asynchronous expression. We are starting the asynchronous code and continuing on with the next expression.

Async.Parallel

We can use Async.Parallel when we have multiple asynchronous objects. This in turn can give some performance gains as multiple tasks are executed concurrently. We’ll use the getUrlContentsizeAsync and query some websites and sort them by size in descending order.

open System.IO
open System.Net

let getUrlContentSizeAsync (url : string) = 
    async {
        let request   = WebRequest.Create (url)
        use! response = request.AsyncGetResponse ()
        use stream    = response.GetResponseStream ()
        use reader    = new StreamReader(stream)
        let contents  = reader.ReadToEnd ()
        return (url,contents.Length)
    }

let sites = ["http://techandwings.ca"; 
             "http://www.cnn.com"; 
             "http://www.foxnews.com";
             "http://msnbc.com";
             "http://google.ca";
             "http://nhl.com";
             "http://mlb.com";
             "http://nfl.com"]

let asyncSites = sites |> List.map getUrlContentSizeAsync

In the REPL you can turn on some statistics analysis by entering the command:

#time;;

Let’s start by executing the list of asyncSites sequentially.

asyncSites |> List.map Async.RunSynchronously |> Seq.sortByDescending snd

The average results of running them sequentially a few times turned out to be around 4 seconds. (Your results may vary)

Real: 00:00:03.720, CPU: 00:00:00.869, GC gen0: 1, gen1: 0
val it : seq =
  seq
    [("http://nhl.com", 537569); ("http://mlb.com", 491099);
     ("http://nfl.com", 196920); ("http://www.cnn.com", 135159); ...]

If we make a couple of changes to the execution we should be able to speed it up. Let’s add Async.Parallel in exchange for the list map.

asyncSites |> Async.Parallel |> Async.RunSynchronously |> Seq.sortByDescending snd

The results of our new execution:

Real: 00:00:01.845, CPU: 00:00:00.820, GC gen0: 1, gen1: 0
val it : seq =
  seq
    [("http://nhl.com", 536551); ("http://mlb.com", 491099);
     ("http://nfl.com", 196920); ("http://www.cnn.com", 135159); ...]

My average results for running with Async.Parallel was just under 2 seconds. We’ve improved the performance of our experiment with a very minor change in how we are executing the asynchronous objects.

Summary

We’ve seen some new ways to run our asynchronous workflows. We have Async.RunSynchronous when we want to wait for the result. We can also start an asynchronous workflow without waiting for anything by using Async.Start. We’ve looked at the ability to chain multiple workflows into a list and execute them either sequentially or in parallel with the help of Async.Parallel. Running multiple asynchronous workflows in parallel can have some very drastic performance improvements. It’s always recommended to test and analyze any kind of enhancements to be sure. A very simple starting point for analysis is using the #time directive. Enabling #time will display some basic statistics about the code we are executing in the REPL.

One thought on “Running with Async

  1. Pingback: Asynchronous Child | Shane Charles

Comments are closed.