Threading model
Windows Presentation Foundation (WPF) is designed to save developers from the difficulties of threading. As a result, most WPF developers don't write an interface that uses more than one thread. Because multithreaded programs are complex and difficult to debug, they should be avoided when single-threaded solutions exist.
No matter how well architected, however, no UI framework is able to provide a single-threaded solution for every sort of problem. WPF comes close, but there are still situations where multiple threads improve user interface (UI) responsiveness or application performance. After discussing some background material, this article explores some of these situations and then concludes with a discussion of some lower-level details.
Note
This topic discusses threading by using the InvokeAsync method for asynchronous calls. The InvokeAsync
method takes an Action or Func<TResult> as a parameter, and returns a DispatcherOperation or DispatcherOperation<TResult>, which has a Task property. You can use the await
keyword with either the DispatcherOperation or the associated Task. If you need to wait synchronously for the Task that is returned by a DispatcherOperation or DispatcherOperation<TResult>, call the DispatcherOperationWait extension method. Calling Task.Wait will result in a deadlock. For more information about using a Task to perform asynchronous operations, see Task-based asynchronous programming.
To make a synchronous call, use the Invoke method, which also has overloads that takes a delegate, Action, or Func<TResult> parameter.
Overview and the dispatcher
Typically, WPF applications start with two threads: one for handling rendering and another for managing the UI. The rendering thread effectively runs hidden in the background while the UI thread receives input, handles events, paints the screen, and runs application code. Most applications use a single UI thread, although in some situations it is best to use several. We'll discuss this with an example later.
The UI thread queues work items inside an object called a Dispatcher. The Dispatcher selects work items on a priority basis and runs each one to completion. Every UI thread must have at least one Dispatcher, and each Dispatcher can execute work items in exactly one thread.
The trick to building responsive, user-friendly applications is to maximize the Dispatcher throughput by keeping the work items small. This way items never get stale sitting in the Dispatcher queue waiting for processing. Any perceivable delay between input and response can frustrate a user.
How then are WPF applications supposed to handle big operations? What if your code involves a large calculation or needs to query a database on some remote server? Usually, the answer is to handle the big operation in a separate thread, leaving the UI thread free to tend to items in the Dispatcher queue. When the big operation is complete, it can report its result back to the UI thread for display.
Historically, Windows allows UI elements to be accessed only by the thread that created them. This means that a background thread in charge of some long-running task cannot update a text box when it is finished. Windows does this to ensure the integrity of UI components. A list box could look strange if its contents were updated by a background thread during painting.
WPF has a built-in mutual exclusion mechanism that enforces this coordination. Most classes in WPF derive from DispatcherObject. At construction, a DispatcherObject stores a reference to the Dispatcher linked to the currently running thread. In effect, the DispatcherObject associates with the thread that creates it. During program execution, a DispatcherObject can call its public VerifyAccess method. VerifyAccess examines the Dispatcher associated with the current thread and compares it to the Dispatcher reference stored during construction. If they don't match, VerifyAccess throws an exception. VerifyAccess is intended to be called at the beginning of every method belonging to a DispatcherObject.
If only one thread can modify the UI, how do background threads interact with the user? A background thread can ask the UI thread to perform an operation on its behalf. It does this by registering a work item with the Dispatcher of the UI thread. The Dispatcher class provides the methods for registering work items: Dispatcher.InvokeAsync, Dispatcher.BeginInvoke, and Dispatcher.Invoke. These methods schedule a delegate for execution. Invoke
is a synchronous call – that is, it doesn't return until the UI thread actually finishes executing the delegate. InvokeAsync
and BeginInvoke
are asynchronous and return immediately.
The Dispatcher orders the elements in its queue by priority. There are ten levels that may be specified when adding an element to the Dispatcher queue. These priorities are maintained in the DispatcherPriority enumeration.
Single-threaded app with a long-running calculation
Most graphical user interfaces (GUIs) spend a large portion of their time idle while waiting for events that are generated in response to user interactions. With careful programming this idle time can be used constructively, without affecting the responsiveness of the UI. The WPF threading model doesn't allow input to interrupt an operation happening in the UI thread. This means you must be sure to return to the Dispatcher periodically to process pending input events before they get stale.
A sample app demonstrating the concepts of this section can be downloaded from GitHub for either C# or Visual Basic.
Consider the following example:
This simple application counts upwards from three, searching for prime numbers. When the user clicks the Start button, the search begins. When the program finds a prime, it updates the user interface with its discovery. At any point, the user can stop the search.
Although simple enough, the prime number search could go on forever, which presents some difficulties. If we handled the entire search inside of the click event handler of the button, we would never give the UI thread a chance to handle other events. The UI would be unable to respond to input or process messages. It would never repaint and never respond to button clicks.
We could conduct the prime number search in a separate thread, but then we would need to deal with synchronization issues. With a single-threaded approach, we can directly update the label that lists the largest prime found.
If we break up the task of calculation into manageable chunks, we can periodically return to the Dispatcher and process events. We can give WPF an opportunity to repaint and process input.
The best way to split processing time between calculation and event handling is to manage calculation from the Dispatcher. By using the InvokeAsync method, we can schedule prime number checks in the same queue that UI events are drawn from. In our example, we schedule only a single prime number check at a time. After the prime number check is complete, we schedule the next check immediately. This check proceeds only after pending UI events have been handled.
Microsoft Word accomplishes spell checking using this mechanism. Spell checking is done in the background using the idle time of the UI thread. Let's take a look at the code.
The following example shows the XAML that creates the user interface.
Important
The XAML shown in this article is from a C# project. Visual Basic XAML is slightly different when declaring the backing class for the XAML.
<Window x:Class="SDKSamples.PrimeNumber"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Prime Numbers" Width="360" Height="100">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
<Button Content="Start"
Click="StartStopButton_Click"
Name="StartStopButton"
Margin="5,0,5,0" Padding="10,0" />
<TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
<TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
</StackPanel>
</Window>
The following example shows the code-behind.
using System;
using System.Windows;
using System.Windows.Threading;
namespace SDKSamples
{
public partial class PrimeNumber : Window
{
// Current number to check
private long _num = 3;
private bool _runCalculation = false;
public PrimeNumber() =>
InitializeComponent();
private void StartStopButton_Click(object sender, RoutedEventArgs e)
{
_runCalculation = !_runCalculation;
if (_runCalculation)
{
StartStopButton.Content = "Stop";
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
else
StartStopButton.Content = "Resume";
}
public void CheckNextNumber()
{
// Reset flag.
_isPrime = true;
for (long i = 3; i <= Math.Sqrt(_num); i++)
{
if (_num % i == 0)
{
// Set not a prime flag to true.
_isPrime = false;
break;
}
}
// If a prime number, update the UI text
if (_isPrime)
bigPrime.Text = _num.ToString();
_num += 2;
// Requeue this method on the dispatcher
if (_runCalculation)
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
private bool _isPrime = false;
}
}
Imports System.Windows.Threading
Public Class PrimeNumber
' Current number to check
Private _num As Long = 3
Private _runCalculation As Boolean = False
Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
_runCalculation = Not _runCalculation
If _runCalculation Then
StartStopButton.Content = "Stop"
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
Else
StartStopButton.Content = "Resume"
End If
End Sub
Public Sub CheckNextNumber()
' Reset flag.
_isPrime = True
For i As Long = 3 To Math.Sqrt(_num)
If (_num Mod i = 0) Then
' Set Not a prime flag to true.
_isPrime = False
Exit For
End If
Next
' If a prime number, update the UI text
If _isPrime Then
bigPrime.Text = _num.ToString()
End If
_num += 2
' Requeue this method on the dispatcher
If (_runCalculation) Then
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
End If
End Sub
Private _isPrime As Boolean
End Class
Besides updating the text on the Button, the StartStopButton_Click
handler is responsible for scheduling the first prime number check by adding a delegate to the Dispatcher queue. Sometime after this event handler has completed its work, the Dispatcher will select the delegate for execution.
As we mentioned earlier, InvokeAsync is the Dispatcher member used to schedule a delegate for execution. In this case, we choose the SystemIdle priority. The Dispatcher will execute this delegate only when there are no important events to process. UI responsiveness is more important than number checking. We also pass a new delegate representing the number-checking routine.
public void CheckNextNumber()
{
// Reset flag.
_isPrime = true;
for (long i = 3; i <= Math.Sqrt(_num); i++)
{
if (_num % i == 0)
{
// Set not a prime flag to true.
_isPrime = false;
break;
}
}
// If a prime number, update the UI text
if (_isPrime)
bigPrime.Text = _num.ToString();
_num += 2;
// Requeue this method on the dispatcher
if (_runCalculation)
StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}
private bool _isPrime = false;
Public Sub CheckNextNumber()
' Reset flag.
_isPrime = True
For i As Long = 3 To Math.Sqrt(_num)
If (_num Mod i = 0) Then
' Set Not a prime flag to true.
_isPrime = False
Exit For
End If
Next
' If a prime number, update the UI text
If _isPrime Then
bigPrime.Text = _num.ToString()
End If
_num += 2
' Requeue this method on the dispatcher
If (_runCalculation) Then
StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
End If
End Sub
Private _isPrime As Boolean
This method checks if the next odd number is prime. If it is prime, the method directly updates the bigPrime
TextBlock to reflect its discovery. We can do this because the calculation is occurring in the same thread that was used to create the control. Had we chosen to use a separate thread for the calculation, we would have to use a more complicated synchronization mechanism and execute the update in the UI thread. We'll demonstrate this situation next.
Multiple windows, multiple threads
Some WPF applications require multiple top-level windows. It's perfectly acceptable for one Thread/Dispatcher combination to manage multiple windows, but sometimes several threads do a better job. This is especially true if there's any chance that one of the windows will monopolize the thread.
Windows Explorer works in this fashion. Each new Explorer window belongs to the original process, but it's created under the control of an independent thread. When Explorer becomes nonresponsive, such as when looking for network resources, other Explorer windows continue to be responsive and usable.
We can demonstrate this concept with the following example.
The top three windows of this image share the same thread identifier: 1. The two other windows have different thread identifiers: Nine and 4. There's a magenta colored rotating ‼️ glyph in the top right of each window.
This example contains a window with a rotating ‼️
glyph, a Pause button, and two other buttons that create a new window under the current thread or in a new thread. The ‼️
glyph is constantly rotating until the Pause button is pressed, which pauses the thread for five seconds. At the bottom of the window, the thread identifier is displayed.
When the Pause button is pressed, all windows under the same thread become nonresponsive. Any window under a different thread continues to work normally.
The following example is the XAML to the window:
<Window x:Class="SDKSamples.MultiWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
<TextBlock.RenderTransform>
<RotateTransform Angle="0" />
</TextBlock.RenderTransform>
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
<Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
<TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
</StackPanel>
<StackPanel Grid.Row="1" Margin="10">
<Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
<Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
</StackPanel>
<StatusBar Grid.Row="2" VerticalAlignment="Bottom">
<StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
</StatusBar>
</Grid>
</Window>
The following example shows the code-behind.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace SDKSamples
{
public partial class MultiWindow : Window
{
public MultiWindow() =>
InitializeComponent();
private void Window_Loaded(object sender, RoutedEventArgs e) =>
ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";
private void PauseButton_Click(object sender, RoutedEventArgs e) =>
Task.Delay(TimeSpan.FromSeconds(5)).Wait();
private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
new MultiWindow().Show();
private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
{
Thread newWindowThread = new Thread(ThreadStartingPoint);
newWindowThread.SetApartmentState(ApartmentState.STA);
newWindowThread.IsBackground = true;
newWindowThread.Start();
}
private void ThreadStartingPoint()
{
new MultiWindow().Show();
System.Windows.Threading.Dispatcher.Run();
}
}
}
Imports System.Threading
Public Class MultiWindow
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
End Sub
Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
Task.Delay(TimeSpan.FromSeconds(5)).Wait()
End Sub
Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
Dim window As New MultiWindow()
window.Show()
End Sub
Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
newWindowThread.SetApartmentState(ApartmentState.STA)
newWindowThread.IsBackground = True
newWindowThread.Start()
End Sub
Private Sub ThreadStartingPoint()
Dim window As New MultiWindow()
window.Show()
System.Windows.Threading.Dispatcher.Run()
End Sub
End Class
The following are some of the details to be noted:
The Task.Delay(TimeSpan) task is used to cause the current thread to pause for five seconds when the Pause button is pressed.
private void PauseButton_Click(object sender, RoutedEventArgs e) => Task.Delay(TimeSpan.FromSeconds(5)).Wait();
Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs) Task.Delay(TimeSpan.FromSeconds(5)).Wait() End Sub
The
SameThreadWindow_Click
event handler immediently shows a new window under the current thread. TheNewThreadWindow_Click
event handler creates a new thread that starts executing theThreadStartingPoint
method, which in turn shows a new window, as described in the next bullet point.private void SameThreadWindow_Click(object sender, RoutedEventArgs e) => new MultiWindow().Show(); private void NewThreadWindow_Click(object sender, RoutedEventArgs e) { Thread newWindowThread = new Thread(ThreadStartingPoint); newWindowThread.SetApartmentState(ApartmentState.STA); newWindowThread.IsBackground = true; newWindowThread.Start(); }
Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs) Dim window As New MultiWindow() window.Show() End Sub Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs) Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint) newWindowThread.SetApartmentState(ApartmentState.STA) newWindowThread.IsBackground = True newWindowThread.Start() End Sub
The
ThreadStartingPoint
method is the starting point for the new thread. The new window is created under the control of this thread. WPF automatically creates a new System.Windows.Threading.Dispatcher to manage the new thread. All we have to do to make the window functional is to start the System.Windows.Threading.Dispatcher.private void ThreadStartingPoint() { new MultiWindow().Show(); System.Windows.Threading.Dispatcher.Run(); }
Private Sub ThreadStartingPoint() Dim window As New MultiWindow() window.Show() System.Windows.Threading.Dispatcher.Run() End Sub
A sample app demonstrating the concepts of this section can be downloaded from GitHub for either C# or Visual Basic.
Handle a blocking operation with Task.Run
Handling blocking operations in a graphical application can be difficult. We don't want to call blocking methods from event handlers because the application appears to freeze up. The previous example created new windows in their own thread, letting each window run independent from one another. While we can create a new thread with System.Windows.Threading.Dispatcher, it becomes difficult to synchronize the new thread with the main UI thread after the work is completed. Because the new thread can't modify the UI directly, we have to use Dispatcher.InvokeAsync, Dispatcher.BeginInvoke, or Dispatcher.Invoke, to insert delegates into the Dispatcher of the UI thread. Eventually, these delegates are executed with permission to modify UI elements.
There's an easier way to run the code on a new thread while synchronizing the results, the Task-based asynchronous pattern (TAP). It's based on the Task and Task<TResult> types in the System.Threading.Tasks
namespace, which are used to represent asynchronous operations. TAP uses a single method to represent the initiation and completion of an asynchronous operation. There are a few benefits to this pattern:
- The caller of a
Task
can choose to run the code asynchronously or synchronously. - Progress can be reported from the
Task
. - The calling code can suspend execution and wait for the result of the operation.
Task.Run example
In this example, we mimic a remote procedure call that retrieves a weather forecast. When the button is clicked, the UI is updated to indicate that the data fetch is in progress, while a task is started to mimic fetching the weather forecast. When the task is started, the button event handler code is suspended until the task finishes. After the task finishes, the event handler code continues to run. The code is suspended, and it doesn't block the rest of the UI thread. The synchronization context of WPF handles suspending the code, which allows WPF to continue to run.
A diagram demonstrating the example app's workflow. The app has a single button with the text "Fetch Forecast." There's an arrow pointing to the next phase of the app after the button is pressed, which is a clock image placed in the center of the app indicating that the app is busy fetching data. After some time, the app returns with either an image of the sun or of rain clouds, depending on the result of the data.
A sample app demonstrating the concepts of this section can be downloaded from GitHub for either C# or Visual Basic. The XAML for this example is quite large and not provided in this article. Use the previous GitHub links to browse the XAML. The XAML uses a single button to fetch the weather.
Consider the code-behind to the XAML:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;
namespace SDKSamples
{
public partial class Weather : Window
{
public Weather() =>
InitializeComponent();
private async void FetchButton_Click(object sender, RoutedEventArgs e)
{
// Change the status image and start the rotation animation.
fetchButton.IsEnabled = false;
fetchButton.Content = "Contacting Server";
weatherText.Text = "";
((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
// Asynchronously fetch the weather forecast on a different thread and pause this code.
string weather = await Task.Run(FetchWeatherFromServerAsync);
// After async data returns, process it...
// Set the weather image
if (weather == "sunny")
weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
else if (weather == "rainy")
weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
//Stop clock animation
((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
//Update UI text
fetchButton.IsEnabled = true;
fetchButton.Content = "Fetch Forecast";
weatherText.Text = weather;
}
private async Task<string> FetchWeatherFromServerAsync()
{
// Simulate the delay from network access
await Task.Delay(TimeSpan.FromSeconds(4));
// Tried and true method for weather forecasting - random numbers
Random rand = new Random();
if (rand.Next(2) == 0)
return "rainy";
else
return "sunny";
}
private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);
private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
}
}
Imports System.Windows.Media.Animation
Public Class Weather
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
' Change the status image and start the rotation animation.
fetchButton.IsEnabled = False
fetchButton.Content = "Contacting Server"
weatherText.Text = ""
DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
' Asynchronously fetch the weather forecast on a different thread and pause this code.
Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
' After async data returns, process it...
' Set the weather image
If weatherType = "sunny" Then
weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
ElseIf weatherType = "rainy" Then
weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
End If
' Stop clock animation
DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
' Update UI text
fetchButton.IsEnabled = True
fetchButton.Content = "Fetch Forecast"
weatherText.Text = weatherType
End Sub
Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
' Simulate the delay from network access
Await Task.Delay(TimeSpan.FromSeconds(4))
' Tried and true method for weather forecasting - random numbers
Dim rand As New Random()
If rand.Next(2) = 0 Then
Return "rainy"
Else
Return "sunny"
End If
End Function
Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
End Sub
Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
End Sub
End Class
The following are some of the details to be noted.
The button event handler
private async void FetchButton_Click(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this); // Asynchronously fetch the weather forecast on a different thread and pause this code. string weather = await Task.Run(FetchWeatherFromServerAsync); // After async data returns, process it... // Set the weather image if (weather == "sunny") weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"]; else if (weather == "rainy") weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"]; //Stop clock animation ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage); ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me) ' Asynchronously fetch the weather forecast on a different thread and pause this code. Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync) ' After async data returns, process it... ' Set the weather image If weatherType = "sunny" Then weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource) ElseIf weatherType = "rainy" Then weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource) End If ' Stop clock animation DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage) DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage) ' Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weatherType End Sub
Notice that the event handler was declared with
async
(orAsync
with Visual Basic). An "async" method allows suspension of the code when an awaited method, such asFetchWeatherFromServerAsync
, is called. This is designated by theawait
(orAwait
with Visual Basic) keyword. Until theFetchWeatherFromServerAsync
finishes, the button's handler code is suspended and control is returned to the caller. This is similar to a synchronous method except that a synchronous method waits for every operation in the method to finish after which control is returned to the caller.Awaited methods utilize the threading context of the current method, which with the button handler, is the UI thread. This means that calling
await FetchWeatherFromServerAsync();
(OrAwait FetchWeatherFromServerAsync()
with Visual Basic) causes the code inFetchWeatherFromServerAsync
to run on the UI thread, but isn't executed on the dispatcher has time to run it, similar to how the Single-threaded app with a long-running calculation example operates. However, notice thatawait Task.Run
is used. This creates a new thread on the thread pool for the designated task instead of the current thread. SoFetchWeatherFromServerAsync
runs on its own thread.Fetching the Weather
private async Task<string> FetchWeatherFromServerAsync() { // Simulate the delay from network access await Task.Delay(TimeSpan.FromSeconds(4)); // Tried and true method for weather forecasting - random numbers Random rand = new Random(); if (rand.Next(2) == 0) return "rainy"; else return "sunny"; }
Private Async Function FetchWeatherFromServerAsync() As Task(Of String) ' Simulate the delay from network access Await Task.Delay(TimeSpan.FromSeconds(4)) ' Tried and true method for weather forecasting - random numbers Dim rand As New Random() If rand.Next(2) = 0 Then Return "rainy" Else Return "sunny" End If End Function
To keep things simple, we don't actually have any networking code in this example. Instead, we simulate the delay of network access by putting our new thread to sleep for four seconds. In this time, the original UI thread is still running and responding to UI events while the button's event handler is paused until the new thread completes. To demonstrate this, we've left an animation running, and you can resize the window. If the UI thread was paused or delayed, the animation wouldn't be shown and you couldn't interact with the window.
When the
Task.Delay
is finished, and we've randomly selected our weather forecast, the weather status is returned to the caller.Updating the UI
private async void FetchButton_Click(object sender, RoutedEventArgs e) { // Change the status image and start the rotation animation. fetchButton.IsEnabled = false; fetchButton.Content = "Contacting Server"; weatherText.Text = ""; ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this); // Asynchronously fetch the weather forecast on a different thread and pause this code. string weather = await Task.Run(FetchWeatherFromServerAsync); // After async data returns, process it... // Set the weather image if (weather == "sunny") weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"]; else if (weather == "rainy") weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"]; //Stop clock animation ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage); ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage); //Update UI text fetchButton.IsEnabled = true; fetchButton.Content = "Fetch Forecast"; weatherText.Text = weather; }
Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs) ' Change the status image and start the rotation animation. fetchButton.IsEnabled = False fetchButton.Content = "Contacting Server" weatherText.Text = "" DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me) ' Asynchronously fetch the weather forecast on a different thread and pause this code. Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync) ' After async data returns, process it... ' Set the weather image If weatherType = "sunny" Then weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource) ElseIf weatherType = "rainy" Then weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource) End If ' Stop clock animation DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage) DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage) ' Update UI text fetchButton.IsEnabled = True fetchButton.Content = "Fetch Forecast" weatherText.Text = weatherType End Sub
When the task finishes and the UI thread has time, the caller of
Task.Run
, the button's event handler, is resumed. The rest of the method stops the clock animation and chooses an image to describe the weather. It displays this image and enables the "fetch forecast" button.
A sample app demonstrating the concepts of this section can be downloaded from GitHub for either C# or Visual Basic.
Technical details and stumbling points
The following sections describe some of the details and stumbling points you may come across with multithreading.
Nested pumping
Sometimes it is not feasible to completely lock up the UI thread. Let's consider the Show method of the MessageBox class. Show doesn't return until the user clicks the OK button. It does, however, create a window that must have a message loop in order to be interactive. While we are waiting for the user to click OK, the original application window does not respond to user input. It does, however, continue to process paint messages. The original window redraws itself when covered and revealed.
Some thread must be in charge of the message box window. WPF could create a new thread just for the message box window, but this thread would be unable to paint the disabled elements in the original window (remember the earlier discussion of mutual exclusion). Instead, WPF uses a nested message processing system. The Dispatcher class includes a special method called PushFrame, which stores an application's current execution point then begins a new message loop. When the nested message loop finishes, execution resumes after the original PushFrame call.
In this case, PushFrame maintains the program context at the call to MessageBox.Show, and it starts a new message loop to repaint the background window and handle input to the message box window. When the user clicks OK and clears the pop-up window, the nested loop exits and control resumes after the call to Show.
Stale routed events
The routed event system in WPF notifies entire trees when events are raised.
<Canvas MouseLeftButtonDown="handler1"
Width="100"
Height="100"
>
<Ellipse Width="50"
Height="50"
Fill="Blue"
Canvas.Left="30"
Canvas.Top="50"
MouseLeftButtonDown="handler2"
/>
</Canvas>
When the left mouse button is pressed over the ellipse, handler2
is executed. After handler2
finishes, the event is passed along to the Canvas object, which uses handler1
to process it. This happens only if handler2
does not explicitly mark the event object as handled.
It's possible that handler2
will take a great deal of time processing this event. handler2
might use PushFrame to begin a nested message loop that doesn't return for hours. If handler2
does not mark the event as handled when this message loop is complete, the event is passed up the tree even though it is very old.
Reentrancy and locking
The locking mechanism of the common language runtime (CLR) doesn't behave exactly as one might imagine; one might expect a thread to cease operation completely when requesting a lock. In actuality, the thread continues to receive and process high-priority messages. This helps prevent deadlocks and make interfaces minimally responsive, but it introduces the possibility for subtle bugs. The vast majority of the time you don't need to know anything about this, but under rare circumstances (usually involving Win32 window messages or COM STA components) this can be worth knowing.
Most interfaces are not built with thread safety in mind because developers work under the assumption that a UI is never accessed by more than one thread. In this case, that single thread may make environmental changes at unexpected times, causing those ill effects that the DispatcherObject mutual exclusion mechanism is supposed to solve. Consider the following pseudocode:
Most of the time that's the right thing, but there are times in WPF where such unexpected reentrancy can really cause problems. So, at certain key times, WPF calls DisableProcessing, which changes the lock instruction for that thread to use the WPF reentrancy-free lock, instead of the usual CLR lock.
So why did the CLR team choose this behavior? It had to do with COM STA objects and the finalization thread. When an object is garbage collected, its Finalize
method is run on the dedicated finalizer thread, not the UI thread. And therein lies the problem, because a COM STA object that was created on the UI thread can only be disposed on the UI thread. The CLR does the equivalent of a BeginInvoke (in this case using Win32's SendMessage
). But if the UI thread is busy, the finalizer thread is stalled and the COM STA object can't be disposed, which creates a serious memory leak. So the CLR team made the tough call to make locks work the way they do.
The task for WPF is to avoid unexpected reentrancy without reintroducing the memory leak, which is why we don't block reentrancy everywhere.
See also
.NET Desktop feedback