What does BeginInvoke do?
According to MSDN, Control.BeginInvoke
"Executes the specified delegate asynchronously on the thread that the control's underlying handle was created on". It basically takes a delegate and runs it on the thread that created the control on which you called BeginInvoke
.
Message queue and Message pumping
- Every Windows application you create is provided with a thread on startup. It's this thread that calls the
Main
method of your application. - For a console application, you can write
Main
to accept user input and process them. - For a GUI application, things are radically different. GUI applications are event based, which means that there needs to be some entity to process and fire events. Windows manages this by creating a message queue for your application. All UI related actions get translated to messages that get posted to this message queue 1. Now, you need someone to read the messages from the queue and call appropriate event handlers. That's what a message pump is for. It basically is a loop that waits for someone to post a message to the queue. Once someone does, it dequeues it and calls the associated event handler.
- In .NET GUI applications, the
Application.Run
method takes care of message pumping. - As an aside, modal dialogs (shown using
Form.ShowDialog
) have their own message pump whereas modeless dialogs (shown usingForm.Show
) don't. Which means that you can callForm.ShowDialog
from any thread butForm.Show
requires the calling thread to be running a message pump. Note that if you callForm.Show
from a UI event handler, both the new form and the current form will be sharing the same message pump, which means that if one form is stuck executing one of the event handlers, the other one won't be usable too.
The One Rule
- One of the cardinal rules of Windows GUI programming is that only the thread that created a control can access and/or modify its contents (except for a few documented exceptions). Try doing it from any other thread and you'll get unpredictable behavior ranging from deadlock, to exceptions to a half updated UI. The right way then to update a control from another thread is to post an appropriate message to the application message queue. When the message pump gets around to executing that message, the control will get updated, on the same thread that created it (remember, the message pump runs on the main thread).
- There are two fundamental Win32 API calls to access and/or modify a control,
SendMessage
andPostMessage
. There is a big difference in the way the two execute. - The major difference is that
SendMessage
blocks the caller till the message gets processed by the message pump whereasPostMessage
returns immediately. - The subtle but important difference is that messages sent using
SendMessage
aren't queued in the message queue whereasPostMessage
messages are. SendMessage
messages are directly "sent" to the message pump. The message pump retrieves and processes messages sent usingSendMessage
before looking into those in the message queue.- Effectively, there are then two queues, one for
SendMessage
messages and one forPostMessage
messages (which is what we call the message queue). The message pump processes all messages in the first queue before starting with the second. - An interesting observation is that if code does a
SendMessage
from within the message pumping thread, the window procedure gets called directly, that is, it doesn't go through the message pump.
Why The One Rule Exists
- In .NET, property/method calls on a
Control
object translate toSendMessage
calls, so you can easily see how it can turn nasty.PostMessage
posts a message to the queue and returns immediately, so there is no chance of deadlock. - There is one other issue with using
SendMessage
that is not obvious at all.SendMessage
, while blocking the current thread, continues to process messages that are sent to the message pump usingSendMessage
or one of the functions that send nonqueued[ ^] messages. This means that your code must be prepared to handle incoming messages when blocked on aSendMessage
call. - To avoid such problems, Windows provides the
SendMessageTimeout
API function, which when called with the right parameters, will prevent pumping of nonqueued messages when blocked on sending a message. - However, all .NET UI controls use
SendMessage
, so you can't avoid the problem, unless you are planning on doing P/Invoke.
Why and when to call BeginInvoke
-
BeginInvoke
essentially does aPostMessage
. - Whenever you want to update a control from a thread that didn't create it, instead of directly calling the method/property to update it, you need to wrap it in a
BeginInvoke
call. - According to MSDN: "There are four methods on a control that are safe to call from any thread:
Invoke
,BeginInvoke
,EndInvoke
, andCreateGraphics
. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread". - the
Control
class provides one property and two methods:InvokeRequired
: Thisbool
property returnstrue
if the thread on which this property is called is not the thread that created this control. Basically, ifInvokeRequired
returnstrue
, you need to call one of the two Invoke methods.
BeginInvoke
: This is a functionally similar to thePostMessage
API function. It posts a message to the queue and returns immediately without waiting for the message to be processed.BeginInvoke
returns anIAsyncResult
, just like theBeginInvoke
method on any delegate. And you can useIAsyncResult
to wait for the message to be processed, just as usual. And you can callEndInvoke
to get return values orout
parameter values, as usual.Invoke
: This is like theSendMessage
API function in that it waits till the message gets processed, but it does not do aSendMessage
internally, it also does aPostMessage
. The difference is that it waits till the delegate is executed on the UI thread before returning. So while there is a chance for the deadlock problem to occur, you can be sure that the other problems associated withSendMessage
won't happen.Invoke
returns the value that the function wrapped by the delegate returned.
- The BCL provides a
MethodInvoker
delegate, which you can use if your wrapped function takes no parameters and returnsvoid
. You can also reuse theEventHandler
delegate in case your wrapped function's signature matches it. The MSDN documentation says using these delegates instead of our own custom delegates will result in faster execution. - Both
BeginInvoke
andInvoke
check if they are called on the correct thread (the thread that created the control) and if so, directly update the control instead of doing thePostMessage
thing. Apart from performance benefits, it also prevents deadlock if you callInvoke
from within a method already running on the UI thread.
Invoke
and BeginInvoke
- There are a few gotcha's with
BeginInvoke
though.- If the function you are calling via
BeginInvoke
accesses shared state (state shared between the UI thread and other threads), you are in trouble. The state might change between the time you calledBeginInvoke
and when the wrapped function actually executes, leading to hard to find timing problems. - If you are passing reference parameters to the function called via
BeginInvoke
, then you must make sure that no one else modifies the passed object before the function completes. Usually, people clone the object before passing it toBeginInvoke
, which avoids the problem altogether.
- If the function you are calling via
- Note that the above points are valid for any function that you run as a thread. They're not so obvious when using
BeginInvoke
, becauseBeginInvoke
doesn't actually create a thread and instead runs the wrapped function on an already existing thread (the UI thread). It still means that there are two threads, so you want to take the same care protecting your shared variables.
A warning
-
Control.BeginInvoke
, which is what we have been discussing so far, works slightly differently fromDelegate.BeginInvoke
. Delegate.BeginInvoke
grabs a threadpool thread and executes the passed delegate on that thread.Control.BeginInvoke
does not use a threadpool thread, it does aPostMessage
to the target window handle and returns.- This is crucial because if it uses threads, then there is no guarantee to the order in which messages are posted and processed by the application.
- Also, unlike
Delegate.BeginInvoke
,Control.BeginInvoke
doesn't require every call toBeginInvoke
to be matched by anEndInvoke
. Of course, if you are using the return value of the method and/orout
orref
values, then you need to call it anyway.
--
Happy day, happy life!
No comments:
Post a Comment