At the recent Professional Developers' Conference, Anders Hejlsberg gave a presentation that demonstrated some of the new language features that Microsoft are working on as part of the next version of Visual C# and Visual Basic. The thrust of the next build of these languages is oriented towards making it easier to write asynchronous code. These new features add native functionality to Visual C# and Visual Basic that are built on top of the TPL, introduced in the .NET Framework 4.0. This article concentrates on the syntax extensions proposed for C#, although similar extensions will be added to Visual Basic.
The purpose of the TPL is to make it easier to build applications that can take advantage of the now-commonplace crop of multicore processors to implement concurrency and multithreading. To recap, using the TPL you can implement a concurrent operation by defining a Task object that references the code to be run concurrently. When you execute the task by using the Start method, the .NET Framework uses its own scheduling algorithm to allocate the Task to a thread and set this thread running at a time convenient to the operating system, when sufficient resources are available. This level of abstraction frees your code from the requirement to understand and manage the workload of your computer (for more information about Tasks, see the post Make Your Applications More Efficient with MultiTasking.)
If you need to perform another operation when a specific task completes, you have a couple of choices:
- You can manually wait for the task to complete by using one of the Wait methods exposed by the Task type. You can then initiate the new operation, possibly by defining another task.
- You can define a continuation. A continuation simply specifies an operation to be performed when a given task completes. The .NET Framework automatically executes the continuation operation as a task that it schedules when the original task finishes. For information about continuations, see the MSDN article TPL and Traditional .NET Asynchronous Programming.
However, although the TPL provides the Task type as an abstraction for a concurrent operation, it is still often necessary to write potentially awkward code to solve some of the common problems that developers frequently encounter when building applications that contain sections that may run concurrently. Additionally, bear in mind that the purpose of the TPL is to implement concurrency (typically by running multiple threads) whereas asynchronicity is a subtley different problem that may require coordinating operations that run on a single thread. This is where the async modifier and the await operator come into play. As an example, consider the following simple WPF window (the XAML definiton is also shown).
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Asynchronous Demonstration" Height="204" Width="525" ResizeMode="NoResize">
<Grid>
<Button Content="Perform Work Synchronously" Height="23" HorizontalAlignment="Left" Margin="15,32,0,0" Name="synchronous" VerticalAlignment="Top" Width="187" Click="synchronous_Click" />
<Rectangle Height="142" HorizontalAlignment="Left" Margin="214,12,0,0" Name="rectangle1" Stroke="Black" VerticalAlignment="Top" Width="280" />
<Label Content="Status:" Height="28" HorizontalAlignment="Left" Margin="223,27,0,0" Name="label1" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="286,27,0,0" Name="status" VerticalAlignment="Top" Width="195" IsEnabled="False" />
<Label Content="Duration:" Height="28" HorizontalAlignment="Left" Margin="223,61,0,0" Name="label2" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" IsEnabled="False" Margin="286,61,0,0" Name="duration" VerticalAlignment="Top" Width="195" />
<Button Content="Perform Work using Async Task" Height="23" HorizontalAlignment="Left" Margin="15,74,0,0" Name="taskBasedAsync" VerticalAlignment="Top" Width="187" Click="taskBasedAsync_Click" />
<Button Content="Perform Work using C# Async" Height="23" HorizontalAlignment="Left" Margin="15,114,0,0" Name="nativeAsync" VerticalAlignment="Top" Width="187" Click="nativeAsync_Click" />
</Grid>
</Window>
This window defines three buttons that each perform the same long-running operation, but by using a different mechanism. The first button performs the work synchronously, in a naive manner, as shown in the following code:
public partial class MainWindow : Window
{
...
private void synchronous_Click(object sender, RoutedEventArgs e)
{
duration.Clear();
status.Text = "Running - Synchronous";
Stopwatch watch = Stopwatch.StartNew();
DoWork();
duration.Text = string.Format("{0} ms", watch.ElapsedMilliseconds);
status.Text = "Completed - Synchronous";
}
...
private void DoWork()
{
Thread.SpinWait(Int32.MaxValue / 2);
}
...
}
This code displays the text "Running – Asynchronous" in the status text box in the right-hand portion of the window before starting a System.Diagnostics.Stopwatch object (the code uses this object to time the duration of the operation). The code then calls the DoWork method, which simply performs a busy wait for a few seconds to simulate a long-running operation, before displaying the time taken and updating the text in the status text box to "Completed – Synchronous". When you run this code, if you click the "Perform Work Synchronously" button, you should observe the following phenomena:
- The Window becomes unresponsive while the DoWork method runs; you cannot even move the window.
- The status text box never actually displays the message "Running – Synchronous". The contents of this text box are only updated when the synchronous_Click method completes, after the contents of the status text box has been overwritten with the message "Completed - Synchronous".
Both of these issues are related to the same problem; the thread running the event loop that waits for Windows messages and handles them is kept busy by the DoWork method, and cannot process any events (such as requests to move the Window) until the synchronous_Click method finishes. Similarly, Windows cannot update the display until the thread running the event loop has finished the synchronous_Click method, so the status text box never displays the text "Running – Synchronous", only the message "Completed - Synchronous".
The usual strategy to avoid these problems and ensure that the user interface remains responsive while the application performs a long-running operation is to run this operation on a separate thread, thus freeing the thread running the Windows event loop to process other messages. You can achieve this with the TPL by using code such as the taskBasedAsync_Click method in the following example. This is the code for the "Perform Work using Async Task" button shown in the earlier image:
public partial class MainWindow : Window
{
...
private void taskBasedAsync_Click(object sender, RoutedEventArgs e)
{
duration.Clear();
status.Text = "Running - Async, Task-Based";
Task t = new Task(() => DoWork());
Stopwatch watch = Stopwatch.StartNew();
t.ContinueWith((task) => UpdateStatus(watch, "Completed - Async, Task-Based"));
t.Start();
}
...
private void DoWork()
{
Thread.SpinWait(Int32.MaxValue / 2);
}
private void UpdateStatus(Stopwatch watch, string message)
{
this.Dispatcher.Invoke(new Action(() =>
{
duration.Text = string.Format("{0} ms", watch.ElapsedMilliseconds);
status.Text = message;
}), DispatcherPriority.ApplicationIdle);
}
...
}
When you run the application and click the "Perform Work using Async Task" button, the user interface remains responsive, even while the DoWork method runs. This is because the DoWork method is performed by a task on a separate thread. Additionally, the status text box successfully displays the message "Running - Async, Task-Based" while the DoWork method runs. The tricky part is arranging for the display to be updated with the time taken to complete the DoWork method and the message " Completed - Async, Task-Based". This is achieved with a continuation that runs when the task performing the DoWork method completes. However, because this task runs on a different thread from that responsible for managing the user interface, it cannot directly modify the properties of the duration and status text boxes, but has to queue updates to the WPF Dispatcher object for the Window instead. When the thread handling the Windows event loop is free, it runs the code specified by the Dispatcher.Invoke method.
So, although this code works, it is messy; if you want to update the user interface, you have to define a continuation that invokes the Dispatch.Invoke method because the continuation does not run on the thread that owns the user interface elements. This is where the async modifier and await operator can prove very useful.
The async modifier to a method indicates that the method contains functionality that can be run asynchronously. The await operator specifies the point at which asynchronous operations can begin inside an async method. As a final example, consider the nativeAsync_Click method shown below. This method runs when the user clicks the "Perform Work using C# Async" button.
public partial class MainWindow : Window
{
...
private async void nativeAsync_Click(object sender, RoutedEventArgs e)
{
duration.Clear();
status.Text = "Running - Async, Native C#";
Task t = new Task(() => DoWork());
Stopwatch watch = Stopwatch.StartNew();
t.Start();
await t;
duration.Text = string.Format("{0} ms", watch.ElapsedMilliseconds);
status.Text = "Completed - Async, Native C#";
}
...
private void DoWork()
{
Thread.SpinWait(Int32.MaxValue / 2);
}
...
}
This code looks like a hybrid combination of the first two examples. It displays the message "Running – Async, Native C#" in the status text box before creating and running a task to perform the DoWork method on a separate thread. However, the await operator is where the clever stuff kicks in. Notice that the nativeAsync_Click method is defined with the async modifier. At run-time, the await operator causes the async method to return immediately from whence it was called without waiting for any subsequent code to run. When the subject of the await operator (the task running the DoWork method) completes, the nativeAsync_Click method resumes running at this point and the statements following the await operator are performed, updating the display with the time taken to perform the operation and the message " Completed - Async, Native C#". In fact, this magic is nothing more than an exercise in the reworking of your code by the C# compiler. When the C# compiler encounters the await operator in an async method, it effectively reformats the code that follows this operator as a continuation that runs on the same thread as the async method. And because the thread that was running the async method was the thread running the Windows event loop, it has direct access to the controls in the Window and can update them directly without routing them through the WPF Dispatcher object for the Window.
Although this approach looks quite simple at first glance, it is important to bear in mind a few points to avoid some possible misconceptions:
- The async modifier does not signify that a method runs asynchronously on a separate thread. All it does is specify that the code in the method can be divided into one or more continuations. When these continuations run, they execute on the same thread as the original method call.
- The await operator specifies the point at which the C# compiler can split the code into a continuation. The await operator itself expects its operand to be an awaitable object. An awaitable object is a type that provides the GetAwaiter method which returns an object that in turn provides the BeginAwait and EndAwait methods. The C# compiler converts your code into statements that uses these methods to create an appropriate continuation. The Visual Studio Async CTP provides extension methods for the Task class, and the code shown in the example above invokes the await operator on a Task object.
To conclude, the async modifier and await operator are powerful constructs that enable you to simplify asynchronous code, and as such they are natural partners with the TPL. Using the async and await keywords, you can concentrate on the logic of your application and let the compiler worry about how to divide your code into one or more continuations.
No comments:
Post a Comment