Thou Shalt Not Break the Golden Rule of Windows Multithreading. Or, Why the Dispatcher Rocks.
I’m in the middle of building a fun little WPF standalone Windows application that needs to build a project from the command-line, using msbuild.exe.
To do this, I’m using System.Diagnostics.Process to execute msbuild.exe in another process. The basic code looks like the following:
<!-- MainWindow – MARKUP -->
<Window
xmlns="https://schemas.microsoft.com/
winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Class="DispatcherConsoleOutputSample.MainWindow"
Title="Dispatcher Console Output Sample"
Width="400" Height="200">
<DockPanel>
<Button Click = "buildAppButton_Click" ... >
Build an App
</Button>
</DockPanel>
</Window>
// MainWindow - CODEBEHIND
using System.Diagnostics; // Process, ProcessStartInfo
using System.Windows; // Window, RoutedEventArgs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
void buildAppButton_Click(
object sender, RoutedEventArgs e)
{
// Create a new process
Process process = newProcess();
// Configure the process to run msbuild.exe to
// build AProject.csproj
process.StartInfo.FileName = @"...\msbuild.exe";
process.StartInfo.Arguments = @"...\AProject.csproj";
// Start the process
process.Start();
}
}
When the application runs and the button is clicked, the code creates a new process in which msbuild.exe is run to compile a C# project. The following figure shows the application in action.
But I didn’t really want the msbuild application’s console output going to a different window. In this case, it was going to be much easier and nicer for a user to see the output appear in the application window. Consequently, the application needed a little update, which included:
1) Hiding the console window in which msbuild was running.
2) Acquiring the console output from msbuild.
3) Displaying the console output from the application window.
Fortunately, this was easy to configure. Here are the updates:
<!-- MainWindow – MARKUP -->
<Window
xmlns="https://schemas.microsoft.com/
winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
x:Class="DispatcherConsoleOutputSample.MainWindow"
Title="Dispatcher Console Output Sample"
Width="400" Height="200">
<DockPanel>
<Button Click = "buildAppButton_Click" ... >
Build an App
</Button>
<!-- TextBox control to show the console output -->
< TextBoxName = "outputTextBox" ... />
</DockPanel>
</Window>
// MainWindow – CODEBEHIND
using System.Diagnostics; // ..., DataReceivedEventHandler
using System.Windows; // Window, RoutedEventArgs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
void buildAppButton_Click(
object sender, RoutedEventArgs e)
{
// Create a new process
Process process = new Process();
// Configure the process to run msbuild.exe to
// build AProject.csproj
process.StartInfo.FileName = @"...\msbuild.exe";
process.StartInfo.Arguments = @"...\AProject.csproj";
// Don't show the console window
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false;
// Capture redirected console output
process.StartInfo.RedirectStandardOutput = true;
process.OutputDataReceived +=
process_OutputDataReceived;
// Start the process
process.Start();
// Read the console output as its generated by msbuild
process.BeginOutputReadLine();
}
void process_OutputDataReceived(
object sender, DataReceivedEventArgs e)
{
// Display console output in text box
this.outputTextBox.Text += e.Data;
}
}
While easy to configure, and seemingly intuitive, there was one little issue that is called out in the following figure:
The problem is that the OutputDataReceived event handler is running on a different thread than the UI (or main) thread. Essentially this is because the event is raised by the process in which msbuild is running. And, as Visual Studio’s debugger was kind enough to point out, this code is violating the golden rule of multithreading:
Thou shalt only update UI using code that runs on the same thread as the UI.
To obey the golden rule, the data needs to be shuttled back to the UI thread so we can update the TextBox on the UI thread, rather than from some other thread.
In WPF, a quick, simple, and nice solution is to use the Dispatcher. Each WPF application will have at least one thread, the UI thread, and a corresponding Dispatcher object associated with it. One of the Dispatcher’s capabilities is to manage a queue of work items for a particular thread. (You can find out more about the Dispatcher’s capabilities here.) All we need to do is to send a work item to the Dispatcher on the UI thread for safe processing. This entails the following tweaks to the code:
// MainWindow – CODEBEHIND
using System.Diagnostics; // ..., DataReceivedEventHandler
using System.Windows; // Window, RoutedEventArgs
using System.Windows.Threading; // Dispatcher,
// DispatcherPriority
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
// NOT HANDLED ON UI THREAD
void process_OutputDataReceived(
object sender, DataReceivedEventArgs e)
{
// Display console output in text box
// NOT ALLOWED FROM A NON-UI THREAD
// this.outputTextBox.Text += e.Data;
// Console output received from process
// running msbuild
// FORWARD TO UI THREAD
this.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
(DispatcherOperationCallback)delegate(object arg)
{
// HANDLED ON UI THREAD
this.outputTextBox.Text += e.Data + "\r\n";
return null;
},
null);
}
}
This code does three things. First, it creates a work item that it sends to the Dispatcher by calling Dispatcher.BeginInvoke. Second, it assigns the work item a normal priority. Third, it provides a delegate that the Dispatcher calls when it processes this work item. In this case, I’m using an anonymous method, although that’s not required. Also, I’m using the DispatcherOperationCallback delegate, since it’s already exists in the .NET Framework, but feel free to use the most appropriate delegate.
The resulting updates work as expected, and as demonstrated by the following figure.
In WPF, when (and if) you need to send messages across threads, your first choice should be the Dispatcher.
Comments
Anonymous
August 13, 2007
PingBack from http://wangmo.wordpress.com/2007/08/13/golden-rule-of-multithreading/Anonymous
July 29, 2010
Just so you know, don't do txtOutput.Text += blah because that made my app climb to 1.6GB of memory and threw an OutOfMemoryException, plus it would cause any new updates to the textbox to block for a few milliseconds, causing hiccups in UI responsiveness. It is much more efficient to use txtOutput.AppendText(blah) as that only increased my memory by only 10MB then went back down.Anonymous
July 29, 2010
Kamran - Thanks for the tip! It's an important thing to keep in mind when working with large amounts of text.