- Overview of Asynchrony
- The Old-Fashioned Way: Event-Based Asynchrony
- The Old-Fashioned Way: The Asynchronous Programming Model
- The Modern Way: The Async Pattern
- Getting Started with Async/ Await
- Exception Handling in Async
- Implementing Task-Based Asynchrony
- Cancellation and Progress
- Asynchronous Lambda Expressions
- Asynchronous I/O File Operations in .NET 4.6
- Debugging Tasks
- Summary
Implementing Task-Based Asynchrony
As you remember from Chapter 41, the Task class provides methods and other members that enable you to execute CPU-intensive work, by splitting code across all the available processors so that most of the code is executed concurrently, when possible. Such members of the Task class return instances of the Task class itself, and therefore can be used along with Await. This possibility has some advantages:
- You can execute synchronous code on a separate thread more easily.
- You can run multiple tasks concurrently and wait for them to complete before making further manipulations.
- You can use Await with CPU-consuming code.
This approach is known as Task-Based Asynchrony, and in this section you learn how to get the most out of it.
Switching Threads
In Chapter 40 you learned how to write code that can run on a separate thread, how to create new threads manually, and how to use the Thread Pool managed by the .NET Framework. With this approach, you run a portion of synchronous code in a separate thread, thus keeping the caller thread-free from an intensive and potentially blocking work. In the .NET Framework 4.6, you have additional alternatives to reach the same objective but writing simpler code. The Task.Run method enables you to run a new task asynchronously, queuing such a task into a thread in the Thread Pool. The result is returned as Task handle for the intensive work, so that you can use Await to wait for the result. Task.Run takes as the first argument a delegate that defines the work that will be executed in the background thread. Such a delegate can be represented either by a method that you point to via the AddressOf clause or by lambdas. In the latter case, the delegate can be a System.Action represented by a statement lambda or a System.Func(Of T) represented by a lambda expression. The following example demonstrates how synchronous code is easily executed in a separate thread by invoking Task.Run:
Private Async Sub RunIntensiveWorkAsync() 'This runs on the UI thread Console.WriteLine("Starting...") 'This runs on a Thread Pool thread Dim result As Integer = Await Task.Run(Function() Dim workResult As Integer = _ SimulateIntensiveWork() Return workResult End Function) 'This runs again on the UI thread Console.WriteLine("Finished") Console.ReadLine() End Sub Private Function SimulateIntensiveWork() As Integer Dim delay As Integer = 5000 Threading.Thread.Sleep(delay) Return delay End Function
While the result of Task.Run is being awaited, the control is immediately returned to the user interface, which remains responsive in the Console window. All the Console.WriteLine and Console.ReadLine statements are executed on the UI thread, whereas the simulated CPU-consuming code runs on the separate thread. Task.Run schedules a new task exactly as Task.Factory.StartNew does; you saw this method in Chapter 41. So, this code has the same effect as using Task.Run:
Dim result As Integer = Await Task.Factory.StartNew(Function() Dim workResult _ As Integer = _ SimulateIntensiveWork() Return workResult End Function)
In summary, Task.Run lets you easily execute intensive computations on a separate thread, taking all the benefits of Await.
Using Combinators
The Task class has other interesting usages, such as managing concurrent operations. This is possible because of two methods, Task.WhenAll and Task.WhenAny, also known as combinators. Task.WhenAll creates a task that will complete when all the supplied tasks complete; Task.WhenAny creates a task that will complete when at least one of the supplied tasks completes. For example, imagine you want to download multiple RSS feeds information from a website. Instead of using Await against individual tasks to complete, you can use Task.WhenAll to continue only after all tasks have completed. The following code provides an example of concurrent download of RSS feeds from the Microsoft Channel 9 feed used previously:
Private Async Sub DownloadAllFeedsAsync() Dim feeds As New List(Of Uri) From {New Uri("http://channel9.msdn.com/Tags/windows+8/RSS"), New Uri("http://channel9.msdn.com/Tags/windows+phone"), New Uri("http://channel9.msdn.com/Tags/visual+basic/RSS")} 'This task completes when all of the requests complete Dim feedCompleted As IEnumerable(Of String) = _ Await Task. WhenAll(From feed In feeds Select New System.Net.WebClient(). DownloadStringTaskAsync(feed)) 'Additional work here... End Sub
This code creates a collection of tasks by sending a DownloadStringTaskAsync request for each feed address in the list of feeds. The task completes (and thus the result of awaiting WhenAll is returned) only when all three feeds have been downloaded, meaning that the complete download result will not be available if only one or two feeds have been downloaded. WhenAny works differently because it creates a task that completes when any of the tasks in a collection of tasks completes. The following code demonstrates how to rewrite the previous example using WhenAny:
Private Async Sub DownloadFeedsAsync() Dim feeds As New List(Of Uri) From {New Uri("http://channel9.msdn.com/Tags/windows+8/RSS"), New Uri("http://channel9.msdn.com/Tags/windows+phone"), New Uri("http://channel9.msdn.com/Tags/visual+basic/RSS")} 'This task completes when any of the requests complete Dim feedCompleted As Task(Of String) = Await Task.WhenAny(From feed In feeds Select New System.Net.WebClient(). DownloadStringTaskAsync(feed)) 'Additional work here... End Sub
In this case a single result will be yielded because the task will be completed when any of the tasks completes. You can also wait for a list of tasks defined as explicit asynchronous methods, like in the following example:
Public Async Sub WhenAnyRedundancyAsync() Dim messages As New List(Of Task(Of String)) From { GetMessage1Async(), GetMessage2Async(), GetMessage3Async() } Dim message = Await Task.WhenAny(messages) Console.WriteLine(message.Result) Console.ReadLine() End Sub Public Async Function GetMessage1Async() As Task(Of String) Await Task.Delay(700) Return "Hi VB guys!" End Function Public Async Function GetMessage2Async() As Task(Of String) Await Task.Delay(600) Return "Hi C# guys!" End Function Public Async Function GetMessage3Async() As Task(Of String) Await Task.Delay(500) Return "Hi F# guys!" End Function
Here you have three asynchronous methods, each returning a string. The code builds a list of tasks including each asynchronous method in the list. Task.WhenAny receives the instance of the collection of tasks as an argument and completes when one of the three methods completes. In this example, you are also seeing for the first time the Task.Delay method. This is the asynchronous equivalent of Thread.Sleep, but while the latter blocks the thread for the specified number of milliseconds, with Task.Delay the thread remains responsive.