Asynchronous Child

Looking back at Running with Async we saw that you can run asynchronous workflows in multiple ways. We can use Async.RunSynchronously when we require the return value. We also looked at Async.Start when we don’t need a result. We also saw an example of running multiple asynchronous workflows using Async.Parallel. Let’s take a moment and get introduced to another means of starting asynchronous workflows.

Starting Children

We can use Async.StartChild when we want to evaluate an asynchronous workflow and get the result at a later time. This can be useful when aggregating data from multiple sources. Imagine having to hit multiple web servers or database servers to pull in data for analysis. We can start the workflows as children and get their results individually.

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

let run1 = async {
        let! child = getUrlContentSizeAsync "http://techandwings.ca" 
                     |> Async.StartChild
        return! child
    }

run1 |> Async.RunSynchronously

In the function run1 we are using Async.StartChild to start the retrieving the content size for the URL. We are immediately returning the result from the child by using return!. Let’s add another URL.

let run2 = async {
        let! child1 = getUrlContentSizeAsync "http://techandwings.ca" 
                      |> Async.StartChild
        let! result1 = child1

        let! child2 = getUrlContentSizeAsync "http://nhl.com" 
                      |> Async.StartChild        
        let! result2 = child2

        return (result1, result2)
    }

run2 |> Async.RunSynchronously

When we start the run2 workflow we now receive a tuple containing the results of the two URLs we are retrieving. We start child1, wait for child1 to finish, start the child, and wait for its results. We’ve created our asynchronous workflow to retrieve the URL sizes sequentially. This is not an ideal use of the workflows. Let’s see if we can improve this by making a minor change.

let run3 = async {
        let! child1 = getUrlContentSizeAsync "http://techandwings.ca" 
                      |> Async.StartChild

        let! child2 = getUrlContentSizeAsync "http://nhl.com" 
                      |> Async.StartChild

        let! result1 = child1        
        let! result2 = child2

        return (result1, result2)
    }

run3 |> Async.RunSynchronously

Looking at run3 we can see we are starting the asynchronous workflows for both URLs before waiting for either result. The retrieval of the URLs’ sizes will happen concurrently. We then wait for the results of the child workflows and return them. This is a more effective use of Async.StartChild as we are now doing execution concurrently which can produce performance benefits when waiting for multiple long running IO operations.

If we enable #time in the REPL we should see a noticeable difference between run2 and run3. The difference is compounded by the Async.Sleep inserted in the getUrlContentSizeAsync function. You should see at least a half second speed up from run3 compared to run2.

Summary

Async.StartChild gives us the ability to control when start the asynchronous workflow and when we want the result. This in turn allows us to execute controlled asynchronous workflows concurrently. Executing asynchronous workflows as children is ideal for aggregating data from multiple sources.