Moving from the Desktop to Devices: Multithreading and the User Interface
Jim Wilson
JW Hedgehog, Inc.
December 2004
Applies to:
Microsoft® .NET Compact Framework
Summary: This article helps developers overcome the limitations of the Microsoft .NET Compact Framework when they use background threads to interact with the user interface. (12 printed pages)
Download UI Safe Invoker Code Sample.msi from the Microsoft Download Center.
Introduction
Multithreading and User Interface Basics
Building a Better Class
Conclusion
I'm writing this month's column as the end of the spring conference season approaches in North America. MDC Canada, MDC United States, Tech-Ed, and the Embedded Developer Conference have all had excellent attendance. I have found it very informative to meet and speak with so many people at these events. Some people have been developing mobile applications for years, but many more people are new to mobility. Most are traditional enterprise developers who have experience building desktop applications with the Microsoft® .NET Framework and are now beginning to build device applications by using the Microsoft .NET Compact Framework.
Those of you who work on both the .NET Compact Framework and the .NET Framework are well aware that despite the tremendous commonality between the two, because of processing power or size reasons, some features are omitted. Although most of these areas aren't problematic, a few are significant enough to create a challenge.
In the conversations I've had with enterprise developers at the conferences, it seems that when developers who use the .NET Framework move to the .NET Compact Framework, they commonly encounter problems in two areas. The first problem is interacting with the user interface (UI) from background threads. The other problem is managing sophisticated deployments, especially those involving the Global Assembly Cache and version forwarding.
Both problems are very important and somewhat involved, so I'm going to spread the discussion over two editions of this column. This month's column will focus on overcoming the limitations of the .NET Compact Framework when you interact with the UI from background threads. Next month's column will be dedicated to deployment, the Global Assembly Cache, and version forwarding.
Many of you may already be familiar with the issues associated with interacting with the UI from background threads, but let's review them quickly as a refresher. Consider the following code example.
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Work1(); // Call Work1 on the current thread
}
void Work1(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // Populates the list box as expected
line = rdr1.ReadLine();
}
}
}
This is a fairly simple example, but it represents a common problem that developers of smart devices face: the need to populate an application UI with data that may be time consuming to retrieve. In this example, the application creates a form that contains a list box and then calls a function, Work1, to populate the list box with the contents of a file.
If the file is small, the application runs well with no surprises. However, if the process of reading the data takes too long, the application may appear to the user to be unresponsive or even frozen. The application's unresponsiveness is an even bigger concern if the application was modified to read the data from a low-bandwidth wireless connection.
A method we have to ensure that the UI remains responsive when a developer performs a lengthy task is to move that task to a background thread. This doesn't make the actual task run any faster, but it does provide a more responsive user experience by allowing other parts of the application to continue while the long-running task runs in the background.
We can easily modify our application to take advantage of multithreading by using the Thread class and the ThreadStart delegate to execute Work1 on a background thread.
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1));
t.Start() ; // Runs Work1 on a background thread
}
void Work1(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // This line is unstable
line = rdr1.ReadLine();
}
}
}
The good news is that our long-running task is now running in the background and therefore doesn't delay or freeze the UI. The bad news is that our application, which was stable before introducing multithreading, now seems to randomly crash. In fact, the program is so unstable that we have no chance of successfully deploying it.
The problem is that all Microsoft Windows® Forms controls in Microsoft .NET have what is known as thread affinity, meaning that their properties and methods can be called only by code running on the same thread that created the control. In the case of our example, lbData was created on the main application thread, but the call to lbData.Items.Add is being made from a background thread. This call to lbData.Items.Add from a background thread results in data corruption.
Note For details about why Windows Forms controls and multithreading need special consideration, see Chris Sells's article Safe, Simple Multithreading in WinForms. The article targets the full .NET Framework, so some solutions that the article offers don't apply to the .NET Compact Framework, but Chris's description of the problem is excellent.
To make our application stable again, we need to modify the code so that all interaction with the list box occurs on the main application thread. We can modify the code by using the Invoke method on the list box. The Invoke method is provided by the System.Windows.Forms.Control base class and is therefore exposed by all Windows Forms controls. The Control.Invoke method runs a delegate on the thread that originally created the control, allowing the delegate to safely interact with the control.
Note Unlike the .NET Framework implementation, which is able to run any delegate, the .NET Compact Framework implementation of Control.Invoke supports only the EventHandler delegate.
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1));
t.Start() ; // Runs Work1 on a background thread
}
private Queue qData = new Queue(); // Visible to all member functions on all threads
void Work1(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lock(qData){ // Synchronize queue acess
qData.Enqueue(line); // Store line value in queue
}
lbData.Invoke(eh); // Transfer control to thread that created lbData
line = rdr1.ReadLine();
}
}
void AddItem(object o, EventArgs e)
{
string line = null;
lock(qData){ // Synchronize queue acess
line = (string)qData(); // Get data from queue
}
lbData.Items.Add(line); // Update list box
}
}
Our application is again stable. We've separated the background task from its interaction with the UI by moving the code that modifies the contents of the list box to the AddItem function and wrapped it in an EventHandler delegate. During each pass through the loop, Work1 places the data read from the file into the qData queue and calls lbData.Invoke to run the EventHandler delegate that is wrapping the AddItem function. Each call to lbData.Invoke suspends running the background thread until the main application thread finishes running the AddItem method. AddItem, running on the main application thread, extracts the value from the queue and safely adds it to the list box.
For simple threading scenarios, the .NET Compact Framework implementation of Control.Invoke works well, but it has notable limitations when compared with the .NET Framework implementation.
First, the .NET Framework provides an overload of Control.Invoke, which accepts an object array. This object array is used to pass parameters to the executed delegate.
By using the Control.Invoke overload in the .NET Framework, we no longer need to use a queue or any other data structure to share data between the threads. The data can simply be passed as part of the call to the delegate, notably simplifying the passing of data between the background and UI threads.
Using the Control.Invoke overload produces the following Work1 and AddItem implementation.
void Work1(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Invoke(eh, new object[]{line, EventArgs.Empty}); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to list box
}
The other major difference is the desktop computer's support for Control.BeginInvoke, which provides asynchronous execution of the delegate. In our application, each time the call to lbData.Invoke is made, the background thread suspends execution until the AddItem method completes. The result is that the application is forced to incur a thread context switch on each iteration of the loop.
In general, we want to minimize thread context switching because the cost to perform it is substantial; the preference is to allow the operating system to choose when to issue a thread context switch. Replacing the call to Control.Invoke with Control.BeginInvoke in the .NET Framework eliminates this forced thread context switch and allows the background thread to continue processing until the operating system decides to perform a thread context switch and run the delegate.
To update our Work1 method to asynchronously run the AddItem delegate, all we need to do is replace the call to lbData.Invoke with lbData.BeginInvoke.
lbData.BeginInvoke(eh, new object[]{line, EventArgs.Empty});
The absence of support for passing parameters and asynchronous execution by the .NET Compact Framework Control class leads to increased complexity and reduced efficiency when we're building multithreaded device applications. This absence is an issue that I find particularly concerning because smart device applications commonly use multithreading. Smart devices also tend to have somewhat limited resources, making easy and efficient multithreading very important.
Because we don't have access to the .NET Compact Framework source code, we can't reasonably add support for parameters and asynchronous delegate execution to the .NET Compact Framework Control class. We can, however, build a new class that provides these capabilities. I call this class UISafeInvoker.
Note The download accompanying this month's column includes complete source code for UISafeInvoker and an application that demonstrates its usage.
In a nutshell, UISafeInvoker is a .NET Compact Framework thread-aware class that provides Invoke and BeginInvoke methods with similar behaviors to the .NET Framework Control class's Invoke and BeginInvoke methods. Although not part of the Control class, the UISafeInvoker.Invoke and UISafeInvoker.BeginInvoke methods are just as easy to use.
Note The author of this column provides UISafeInvoker as a code sample. Microsoft provides no support for this code, nor is there any warranty, explicit or implied, for the use of this class.
Unlike the desktop computer's Control.Invoke and BeginInvoke methods, which can execute any kind of delegate, UISafeInvoker, like the .NET Compact Framework Control.Invoke and BeginInvoke methods, supports only the EventHandler delegate. Because UISafeInvoker supports only one delegate type, there is no need to use an object array to pass parameters. Instead, Invoke and BeginInvoke accept object and EventArgs parameters that are passed directly to the corresponding arguments in the EventHandler delegate. Here are the signatures for each method.
void Invoke(EventHandler eh, object obj, EventArgs eArgs);
IAsyncResult BeginInvoke(EventHandler eh, object obj, EventArgs eArgs);
Using UISafeInvoker is as simple as declaring a reference within the Form class and creating an instance within the Form constructor. After it is created, UISafeInvoker internally tracks the thread on which it is created, so the Invoke and BeginInvoke methods can run the desired delegate on that same thread. Creating UISafeInvoker as part of the Form constructor creates it on the same thread as all of the form controls; therefore, any delegate that the Invoke or BeginInvoke method runs can safely update the UI controls.
Here's our test application updated to use UISafeInvoker.
class MyForm : Form{
ListBox lbData ;
UISafeInvoker invoker ; // Declare UISafeInvoker
MyForm() {
InitializeComponent();
invoker = new UISafeInvoker(); // Create UISafeInvoker on main UI thread
Thread t = new Thread(new ThreadStart(Work1));
t.Start() ; // Runs Work1 on a background thread
}
void Work1(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
invoker.BeginInvoke(eh, line, EventArgs.Empty); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to Listbox
}
}
With UISafeInvoker, our .NET Compact Framework application has overcome the limitations of the .NET Compact Framework Control.Invoke method and now provides easy and efficient interthread communication. This communication is similar to that of the .NET Framework, without the need for extra data structures or complex coding.
Internally, UISafeInvoker is fairly straightforward because it has to do only two things: keep track of the thread that created it and provide a reliable way to transfer data between threads.
The solution — though it may sound dated — is based on window messages. All windows that the Windows operating system creates have a message queue. An application can place messages in that queue by using the Microsoft Win32® SDK functions SendMessage and PostMessage. These functions allow the application to pass an integer flag that identifies the action to perform and two message-defined data values historically known as WParam and LParam.
The SendMessage and PostMessage functions are basically the same, with one key difference. SendMessage places the message in the window message queue, blocking the message until the window finishes processing it. PostMessage places the message in the window message queue and returns immediately. SendMessage and PostMessage can be called from any thread, but the window always processes the messages on the thread that created the window. This behavior implicitly solves our problem of thread tracking and transferring data between threads.
All UI controls are windows, but applications can also create hidden windows that have the ability to process messages but do not render anything visible on the screen. The MessageWindow class exposes the functionality of implementing a hidden window to the .NET Compact Framework.
Note If you're interested in the details of how message queues and message processing work, see About Messages and Message Queues. For more information about the MessageWindow class, see Compact Framework Unique Classes.
Fundamentally, UISafeInvoker is simply an encapsulation of a hidden window and calls to SendMessage and PostMessage. Here's the skeleton of the UISafeInvoker implementation.
public class UISafeInvoker : MessageWindow
{
const int WM_USER = 1024; // Traditional start of application-defined messages
const int WM_INVOKEMETHOD = WM_USER + 1; // Our special message
// Handle window message processing
protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
if (m.Msg == WM_INVOKEMETHOD)
{
// Get data from message
// Run delegate
}
}
// Instigate delegate execution and wait for completion
public void Invoke(EventHandler eh, ...)
{
Message m = Message.Create(this.Hwnd,
WM_INVOKEMETHOD, ...);
MessageWindow.SendMessage(ref m);
}
// Instigate delegate execution and return immediately
public void BeginInvoke(EventHandler eh, ...)
{
Message m = Message.Create(this.Hwnd,
WM_INVOKEMETHOD, ...);
MessageWindow.PostMessage(ref m);
}
}
Both the Invoke and BeginInvoke methods send messages to the hidden window that contains information about the delegate to run. The Invoke method uses SendMessage and therefore runs synchronously. BeginInvoke provides asynchronous execution because PostMessage places the message in the window message queue and returns immediately.
The WndProc method is called each time the hidden window receives a message and is responsible for running the desired delegate. Because it is called as part of the window's message processing, the thread that created the window always executes the WndProc method. Because the code always runs on this same thread, the delegate that it runs can safely interact with UI controls created on the same thread.
One challenging aspect of the UISafeInvoker implementation is passing data from the Invoke and BeginInvoke methods to the WndProc method. To send data to the hidden window by using SendMessage or PostMessage, we first need to define a class that contains the information necessary to store a reference to an EventHandler delegate, along with the object and EventArgs parameters that the EventHandler delegate expects.
class InvokerData{
public EventHandler eventHandler;
public object obj;
public EventArgs eventArgs;
}
Our Invoke and BeginInvoke implementations can then populate an instance of the InvokerData class and use SendMessage or PostMessage to send that information to the WndProc method.
The problem is that InvokerData is a .NET class stored in the managed memory space of the .NET Compact Framework. The implementation of SendMessage and PostMessage and the window message queue are handled by the underlying Windows operating system, which is outside the .NET Compact Framework managed memory space.
Remember that the .NET Compact Framework actively tracks and manages the memory for all application objects. It may move objects from one location in memory to another, and it deletes any object that has no active .NET references pointing to it.
The complication we have when we use SendMessage and PostMessage is that if we were to pass the InvokerData object reference as a parameter to one of these functions, the current address of the object would be copied directly into the hidden window message queue. The window message queue being implemented by the Windows operating system is outside the .NET Compact Framework managed memory space; therefore, the .NET Compact Framework has no awareness of the fact that the application is still holding the address of the InvokerData object and plans to reuse it.
When the hidden window eventually processes the message, the address is copied from the window message queue into the .NET Compact Framework Message structure received by the UISafeInvoker WndProc function. During the time between when the address is copied to the message queue and when it is passed back into the WndProc function, the .NET Compact Framework garbage collector may move or even possibly delete the InvokerData object.
To avoid this potentially dangerous scenario, we can simply ask the .NET Compact Framework to give us some kind of token that represents the InvokerData object that is safe to transport outside the .NET environment and will still be valid when we return. To do this, we use the GCHandle structure.
The GCHandle structure provides a facility to create a token that represents a .NET object that can be safely passed outside the .NET environment and later used to locate that same object. Having a GCHandle structure to a .NET object prevents the object from being garbage collected. The GCHandle structure can also be safely cast to and from IntPtr, making it easy to use with SendMessage and PostMessage.
Now that we have GCHandle, here's the complete UISafeInvoker.Invoke method; UISafeInvoker.BeginInvoke is basically the same except that it uses PostMessage.
public void Invoke(EventHandler eh, object obj, EventArgs e){
InvokerData d = new InvokerData() ;
d.eventHandler eh;
d.obj = sender;
d.eventArgs = e;
GCHandle dataHandle = GCHandle.Alloc(d); // Get token to InvokerData
IntPtr iPtr = (IntPtr) dataHandle; // Cast to IntPtr
Message m = Message.Create(this.Hwnd, WM_INVOKEMETHOD, IntPtr.Zero, iPtr);
MessageWindow.SendMessage(ref m);
}
Our implementation of WndProc can then retrieve the InvokerData instance by using the GCHandle structure contained in the received message.
Note The WndProc function must call the Free method on the GCHandle structure when the structure is no longer needed. If the Free method is not called, the .NET Compact Framework memory manager has no way of knowing that we no longer need the GCHandle and will not be able to clean up the associated object.
protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
if (m.Msg == WM_INVOKEMETHOD) {
GCHandle h = (GCHandle) m.LParam;// Cast IntPtr back to GCHandle
InvokerData d = (InvokerData) h.Target; // Get the InvokerData instance
h.Free(); // Indicate that we are finished with GCHandle
d.eventHandler(d.obj, d.eventArgs); // Call the delegate passing the parameters
}
}
There are a number of differences between the .NET Framework and the .NET Compact Framework. Most differences are subtle; a few are more complex, requiring a little extra creativity. Although interacting with the UI from a background thread is one of the more complex cases, our UISafeInvoker class gives us the ability to pass parameters and asynchronously run delegates across threads, just as the .NET Framework does.
Before I close, I want to thank everyone who attended my conference sessions this season. I hope you enjoyed attending the sessions as much as I enjoyed giving them, and I look forward to seeing everyone next season. In the meantime, I hope you'll continue to join me here each month.
I also want to extend a special thanks to all those who asked me so many great questions. Interacting with everyone and hearing about people's real-world issues is one of the most rewarding parts of what I do. If you have a question or topic that you want to see discussed here in You Can Take It with You, please send me mail at jimw@jwhh.com.
Please join me next month, when we'll look at another area where .NET Framework developers commonly have difficulty moving to the .NET Compact Framework: deployment, the Global Assembly Cache, and version forwarding.