What does SynchronizationContext do?
In the book Programming C#, it has some sample code about SynchronizationContext
:
SynchronizationContext originalContext = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate {
string text = File.ReadAllText(@"c:\temp\log.txt");
originalContext.Post(delegate {
myTextBox.Text = text;
}, null);
});
I'm a beginner in threads, so please answer in detail.
First, I don't know what does context mean, what does the program save in the originalContext
? And when the Post
method is fired, what will the UI thread do?
If I ask some silly things, please correct me, thanks!
EDIT: For example, what if I just write myTextBox.Text = text;
in the method, what's the difference?
What does SynchronizationContext do?
Simply put, SynchronizationContext
represents a location "where" code might be executed. Delegates that are passed to its Send
or Post
method will then be invoked in that location. (Post
is the non-blocking / asynchronous version of Send
.)
Every thread can have a SynchronizationContext
instance associated with it. The running thread can be associated with a synchronization context by calling the static SynchronizationContext.SetSynchronizationContext
method, and the current context of the running thread can be queried via the SynchronizationContext.Current
property.
Despite what I just wrote (each thread having an associated synchronization context), a SynchronizationContext
does not necessarily represent a specific thread; it can also forward invocation of the delegates passed to it to any of several threads (e.g. to a ThreadPool
worker thread), or (at least in theory) to a specific CPU core, or even to another network host. Where your delegates end up running is dependent on the type of SynchronizationContext
used.
Windows Forms will install a WindowsFormsSynchronizationContext
on the thread on which the first form is created. (This thread is commonly called "the UI thread".) This type of synchronization context invokes the delegates passed to it on exactly that thread. This is very useful since Windows Forms, like many other UI frameworks, only permits manipulation of controls on the same thread on which they were created.
What if I just write
myTextBox.Text = text;
in the method, what's the difference?
The code that you've passed to ThreadPool.QueueUserWorkItem
will be run on a thread pool worker thread. That is, it will not execute on the thread on which your myTextBox
was created, so Windows Forms will sooner or later (especially in Release builds) throw an exception, telling you that you may not access myTextBox
from across another thread.
This is why you have to somehow "switch back" from the worker thread to the "UI thread" (where myTextBox
was created) before that particular assignment. This is done as follows:
-
While you are still on the UI thread, capture Windows Forms'
SynchronizationContext
there, and store a reference to it in a variable (originalContext
) for later use. You must querySynchronizationContext.Current
at this point; if you queried it inside the code passed toThreadPool.QueueUserWorkItem
, you might get whatever synchronization context is associated with the thread pool's worker thread. Once you have stored a reference to Windows Forms' context, you can use it anywhere and at any time to "send" code to the UI thread. -
Whenever you need to manipulate a UI element (but are not, or might not be, on the UI thread anymore), access Windows Forms' synchronization context via
originalContext
, and hand off the code that will manipulate the UI to eitherSend
orPost
.
Final remarks and hints:
-
What synchronization contexts won't do for you is telling you which code must run in a specific location / context, and which code can just be executed normally, without passing it to a
SynchronizationContext
. In order to decide that, you must know the rules and requirements of the framework you're programming against — Windows Forms in this case.So remember this simple rule for Windows Forms: DO NOT access controls or forms from a thread other than the one that created them. If you must do this, use the
SynchronizationContext
mechanism as described above, orControl.BeginInvoke
(which is a Windows Forms-specific way of doing exactly the same thing). -
If you're programming against .NET 4.5 or later, you can make your life much easier by converting your code that explicitly uses
SynchronizationContext
,ThreadPool.QueueUserWorkItem
,control.BeginInvoke
, etc. over to the newasync
/await
keywords and the Task Parallel Library (TPL), i.e. the API surrounding theTask
andTask<TResult>
classes. These will, to a very high degree, take care of capturing the UI thread's synchronization context, starting an asynchronous operation, then getting back onto the UI thread so you can process the operation's result.
I'd like to add to other answers, SynchronizationContext.Post
just queues a callback for later execution on the target thread (normally during the next cycle of the target thread's message loop), and then execution continues on the calling thread. On the other hand, SynchronizationContext.Send
tries to execute the callback on the target thread immediately, which blocks the calling thread and may result in deadlock. In both cases, there is a possibility for code reentrancy (entering a class method on the same thread of execution before the previous call to the same method has returned).
If you're familiar with Win32 programming model, a very close analogy would be PostMessage
and SendMessage
APIs, which you can call to dispatch a message from a thread different from the target window's one.
Here is a very good explanation of what synchronization contexts are: It's All About the SynchronizationContext.
It stores the synchronization provider, a class derived from SynchronizationContext. In this case that will probably be an instance of WindowsFormsSynchronizationContext. That class uses the Control.Invoke() and Control.BeginInvoke() methods to implement the Send() and Post() methods. Or it can be DispatcherSynchronizationContext, it uses Dispatcher.Invoke() and BeginInvoke(). In a Winforms or WPF app, that provider is automatically installed as soon as you create a window.
When you run code on another thread, like the thread-pool thread used in the snippet, then you have to be careful that you don't directly use objects that are thread-unsafe. Like any user interface object, you must update the TextBox.Text property from the thread that created the TextBox. The Post() method ensures that the delegate target runs on that thread.
Beware that this snippet is a bit dangerous, it will only work correctly when you call it from the UI thread. SynchronizationContext.Current has different values in different threads. Only the UI thread has a usable value. And is the reason the code had to copy it. A more readable and safer way to do it, in a Winforms app:
ThreadPool.QueueUserWorkItem(delegate {
string text = File.ReadAllText(@"c:\temp\log.txt");
myTextBox.BeginInvoke(new Action(() => {
myTextBox.Text = text;
}));
});
Which has the advantage that it works when called from any thread. The advantage of using SynchronizationContext.Current is that it still works whether the code is used in Winforms or WPF, it matters in a library. This is certainly not a good example of such code, you always know what kind of TextBox you have here so you always know whether to use Control.BeginInvoke or Dispatcher.BeginInvoke. Actually using SynchronizationContext.Current is not that common.
The book is trying to teach you about threading, so using this flawed example is okayish. In real life, in the few cases where you might consider using SynchronizationContext.Current, you'd still leave it up to C#'s async/await keywords or TaskScheduler.FromCurrentSynchronizationContext() to do it for you. But do note that they still misbehave the way the snippet does when you use them on the wrong thread, for the exact same reason. A very common question around here, the extra level of abstraction is useful but makes it harder to figure out why they don't work correctly. Hopefully the book also tells you when not to use it :)