Updated on 2020-04-29
This tip shows you how to do UI updates without having to worry about locking.
Typically, Windows Forms and controls themselves are not thread safe, which can greatly complicate things in a multithreaded application. This article presents an alternative to thread synchronization using marshalling to execute some code invoked from an auxiliary thread on the main UI thread.
Threading is difficult, and very easy to get wrong, and when there are synchronization issues, they are murder to track down. If at all reasonable, generally we should avoid multithreading wherever possible if we want robust code.
A very common case where multithreading can complicate things is updating the user interface, which typically must be done from the UI thread since Winforms are not thread safe.
We're going to avoid the synchronization issue entirely by marshalling some code from the calling thread to the main UI thread, causing the code itself to execute on the UI thread. Since everything UI related is then being done on the UI thread, there are no synchronization issues.
Enter ISynchronizeInvoke. This little interface is a contract that says "hey, I can accept delegates and execute them on this thread" where "this thread" is the thread on which the implementation of ISynchronizeInvoke was created. Since every form and every control implements this interface, every control can marshal delegate code to its thread (the UI thread). This is extremely useful as we'll see.
The code is pretty basic once you get the hang of it.
ISynchronizeInvoke implements four significant members:
Invoke()
(both overloads) invokes the code in the given delegate on the thread where this instance was createdBeginInvoke()
and EndInvoke()
are the asynchronous versions of the above - BeginInvoke()
, unlike Invoke()
does not block.InvokeRequired
indicates whether or not we must use Invoke()
/BeginInvoke()
to execute safely. If it's false, we don't have to marshal the delegate, we can call it directly. The only thing this is good for is performance. It's not strictly necessary to use it.Let's look at the source:
// create a thread. we use this instead of tasks
// or thread pooling simply for demonstration.
// normally, it's not a great idea to spawn
// threads directly like this.
var thread = new Thread(() => {
// fill the progress bar, slowly
for(var i = 0;i<10001;++i)
{
// update the progress bar in a thread safe manner
// you can't use a lambda here without assigning
// it to a specific delegate type. Here, we use
// Action since it doesn't need any arguments
// or a return value
Action action = () =>
{
// execute this code on the UI thread
Progress.Value = i / 100;
};
// this marshals the code above onto the thread
// that this form is running on (the UI thread)
// everything within the action code is executed
// on the UI thread. We only do this if
// InvokeRequired is true, otherwise we can
// invoke the delegate directly. In this example,
// it should always be true, but in the real world,
// it will not be necessarily. Calling Invoke when
// it's not necessary (InvokeRequired=false) doesn't
// hurt, but it causes unnecessary overhead.
if (InvokeRequired)
Invoke(action);
else
action();
// all controls and forms implement Invoke.
// There is also BeginInvoke/EndInvoke which
// you can use to invoke asynchronously
}
});
thread.Start();
I've heavily commented this for reference. Basically, what we're doing here is spawning a thread, and we want that thread to update the progress bar as it goes. However, the progress bar is on the UI thread, so it's not safe to invoke from another thread.
In order to get around this, rather than implement thread synchronization, we can simply use Invoke() as above, which will cause all the code in action to be executed on the UI thread, using the form's ISynchronizeInvoke.Invoke() method. Note that we can't use a lambda or anonymous delegate directly with Invoke() or BeginInvoke(). That's why we hold it in an Action delegate name action. Sure it's a little clunky, but it's a vast improvement over having to implement thread safe calls to the UI.
That's all there is to it - and that's good! The easier the better, especially when it comes to thread. Enjoy!