May 10th, 2011

Simultaneous Async Tasks (Alan Berman)

The new Async feature in the Visual Studio Async CTP (SP1 Refresh) provides an elegantly simple technique to make code asynchronous.

Our writing team uses an internal app that would benefit from asynchronous calls.  For each URL contained in the MSDN documentation that we publish, the app lists the title from the link, and the title parsed from HTML in the downloaded web page.  We use the app to verify that URL links are valid.

The following example is a very simplified version of the relevant code, which does synchronous reads of multiple web pages.

 

Imports System.Net
Imports System.Threading.Tasks

Module Module1
    Sub Main()
        Dim urls As List(Of String) = BuildURLs()

        Dim startTime = Date.Now

        GetWebPagesSynchronous(urls)
        ‘GetWebPagesAsync(urls).Wait()

        Dim seconds = (Date.Now – startTime).TotalSeconds

        Console.WriteLine()
        Console.WriteLine(“Ended in ” & seconds & ” seconds”)
        Console.ReadKey()
    End Sub

    Private Sub GetWebPagesSynchronous(urls As List(Of String))
        Dim client As New WebClient

        For Each url In urls
            Dim text = client.DownloadString(New Uri(url))
            Console.WriteLine(GetTitle(text))
        Next
    End Sub

    Private Function GetTitle(input As String) As String
        Const startText = “<title>”
        Const endText = “</title>”

        Dim startIndex = input.IndexOf(startText)
        If startIndex = -1 Then
            Return “not found”
        Else
            startIndex += startText.Length
            Dim endIndex = input.IndexOf(endText, startIndex)
            Dim result = input.Substring(startIndex, endIndex – startIndex)
            Return result.Trim()
        End If
    End Function

    Private Function BuildURLs() As List(Of String)
        Return New List(Of String) From
            {
            “
http://www.microsoft.com”,
            “
http://msdn.com”
            }
    End Function
End Module

 

The following method transforms the above example to use the Async feature.  Starting with the above example, change Main to call GetWebPagesAsync(urls).Wait() instead of GetWebPagesSynchronous(urls).

In your project, add a reference to AsyncCtpLibrary.dll, which is in My DocumentsMicrosoft Visual Studio Async CTPSamples.

 

Private Async Function GetWebPagesAsync(urls As List(Of String)) As Task

    Dim theTasks As New List(Of Task(Of String))

    For Each url In urls
        Dim client As New WebClient()

        Dim theTask As Task(Of String) =
            client.DownloadStringTaskAsync(New Uri(url))

        theTasks.Add(theTask)
    Next

    ‘ Wait until the tasks are done.

    ‘ The Await statement causes execution to immediately return
    ‘ to the calling method, returning a new task. When all of the
    ‘ tasks complete, execution continues within this method.

    ‘ TaskEx.WhenAll would normally be Task.WhenAll.
    ‘ It’s TaskEx.WhenAll in the CTP only.
    Await TaskEx.WhenAll(theTasks)

    For Each theTask In theTasks
        Console.WriteLine(GetTitle(theTask.Result))
    Next
End Function

 

For each URL in the list, the DownloadStringTaskAsync method returns a Task object that is then stored in a generic List.  The code waits for completion of all of the tasks by using an Await statement and the Task.WhenAll method. The WhenAll method accepts an object that implements IEnumerable(T), including List(T).

The code spins up all of these tasks without worrying about running out of resources from a language perspective.  I was inclined to write code to carefully throttle the calls to conserve resources, but was told that is unnecessary.

On computer running Window s 7, at a randomly convenient time, I ran the above console apps five times synchronously and five times asynchronously, with 20 URLs and 100 URLs. For 20 URLs, the average time to complete was 7.2 seconds for synchronous and 3.2 seconds for Async.  For 100 URLs, the average was 39.5 seconds for synchronous, and 28.5 seconds for Async.  With 100 URLs on another day, the average was 39.9 seconds for synchronous, and 13.5 seconds for Async.  The results are specific to the conditions, and your results will vary.

Note that the speedup in the Async example is almost entirely from the parallel processing, not the asynchronous processing.  The advantages of asynchrony are that it does not tie up multiple threads, and that it does not tie up the user interface thread.

Cancellation

The following example adds cancellation.  To actually cancel the operation, modify the code to change cancelIt = False to cancelIt = True, and change the Thread.Sleep call to specify the milliseconds before cancellation.

This code creates a CancellationTokenSource that contains a CancellationToken. The same cancellation token is passed to every call to DownloadStringTaskAsync. The CancellationTokenSource is also used to invoke cancellation.

 

Imports System.Net
Imports System.Threading
Imports System.Threading.Tasks

Module Module1
    Sub Main()
        Dim urls As List(Of String) = BuildURLs()

        Dim startTime = Date.Now

        ProcessAsyncCancellable(urls)

        Dim seconds = (Date.Now – startTime).TotalSeconds

        Console.WriteLine()
        Console.WriteLine(“Ended in ” & seconds & ” seconds”)
        Console.ReadKey()
    End Sub

    Private Sub ProcessAsyncCancellable(urls As List(Of String))
        Dim cts As New CancellationTokenSource()

        cts.Token.Register(Sub() Console.WriteLine(“cancelling”))

        Dim theTask As Task = GetWebPagesAsync(urls, cts)

        ‘ To cancel midstream, set cancelIt to True.
        Dim cancelIt = False
        If cancelIt Then
            ‘ Set the milliseconds before cancelling.
            Thread.Sleep(2000)
            cts.Cancel()
        Else
            theTask.Wait()
        End If
    End Sub

    Private Async Function GetWebPagesAsync( _
    urls As List(Of String),
    cts As CancellationTokenSource) As Task

        Dim theTasks As New List(Of Task(Of String))

        For Each webAddress In urls
            Dim client As New WebClient()

            Dim theTask As Task(Of String) =
                client.DownloadStringTaskAsync(New Uri(webAddress), cts.Token)

            theTasks.Add(theTask)
        Next

        Await TaskEx.WhenAll(theTasks)

        For Each theTask In theTasks
            Console.WriteLine(GetTitle(theTask.Result))
        Next
    End Function

    Private Function BuildURLs() As List(Of String)
        Return New List(Of String) From
            {
            “
http://www.microsoft.com”,
            “
http://msdn.com”
            }
    End Function
    Private Function GetTitle(input As String) As String
        Const startText = “<title>”
        Const endText = “</title>”

        Dim startIndex = input.IndexOf(startText)
        If startIndex = -1 Then
            Return “not found”
        Else
            startIndex += startText.Length
            Dim endIndex = input.IndexOf(endText, startIndex)
            Dim result = input.Substring(startIndex, endIndex – startIndex)
    &nbs p;       Return result.Trim()
        End If
    End Function
End Module

 

The above example displays all of the titles only when all of the tasks are complete. If you want to see the results before cancellation, you can replace GetWebPagesAsync with the following:

 

Private Async Function GetWebPagesAsync( _
urls As List(Of String),
cts As CancellationTokenSource) As Task

    For Each webAddress In urls
        Dim client As New WebClient()

        Dim theTask As Task(Of String) =
            client.DownloadStringTaskAsync(New Uri(webAddress), cts.Token)

        Await theTask
        Console.WriteLine(GetTitle(theTask.Result))
    Next
End Function

 

Thanks to Anthony Green, Alex Turner, Lucian Wischik, Mick Alberts, and Thomas Petchel for providing tech review.

 

Resources

Author

0 comments