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
Mainmethod of your application. - For a console application, you can write
Mainto 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.Runmethod 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.ShowDialogfrom any thread butForm.Showrequires the calling thread to be running a message pump. Note that if you callForm.Showfrom 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,
SendMessageandPostMessage. There is a big difference in the way the two execute. - The major difference is that
SendMessageblocks the caller till the message gets processed by the message pump whereasPostMessagereturns immediately. - The subtle but important difference is that messages sent using
SendMessagearen't queued in the message queue whereasPostMessagemessages are. SendMessagemessages are directly "sent" to the message pump. The message pump retrieves and processes messages sent usingSendMessagebefore looking into those in the message queue.- Effectively, there are then two queues, one for
SendMessagemessages and one forPostMessagemessages (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
SendMessagefrom 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
Controlobject translate toSendMessagecalls, so you can easily see how it can turn nasty.PostMessageposts a message to the queue and returns immediately, so there is no chance of deadlock. - There is one other issue with using
SendMessagethat is not obvious at all.SendMessage, while blocking the current thread, continues to process messages that are sent to the message pump usingSendMessageor one of the functions that send nonqueued[ ^] messages. This means that your code must be prepared to handle incoming messages when blocked on aSendMessagecall. - To avoid such problems, Windows provides the
SendMessageTimeoutAPI 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
-
BeginInvokeessentially 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
BeginInvokecall. - 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
Controlclass provides one property and two methods:InvokeRequired: Thisboolproperty returnstrueif the thread on which this property is called is not the thread that created this control. Basically, ifInvokeRequiredreturnstrue, you need to call one of the two Invoke methods.
BeginInvoke: This is a functionally similar to thePostMessageAPI function. It posts a message to the queue and returns immediately without waiting for the message to be processed.BeginInvokereturns anIAsyncResult, just like theBeginInvokemethod on any delegate. And you can useIAsyncResultto wait for the message to be processed, just as usual. And you can callEndInvoketo get return values oroutparameter values, as usual.Invoke: This is like theSendMessageAPI function in that it waits till the message gets processed, but it does not do aSendMessageinternally, 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 withSendMessagewon't happen.Invokereturns the value that the function wrapped by the delegate returned.
- The BCL provides a
MethodInvokerdelegate, which you can use if your wrapped function takes no parameters and returnsvoid. You can also reuse theEventHandlerdelegate 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
BeginInvokeandInvokecheck if they are called on the correct thread (the thread that created the control) and if so, directly update the control instead of doing thePostMessagething. Apart from performance benefits, it also prevents deadlock if you callInvokefrom within a method already running on the UI thread.
Invoke and BeginInvoke
- There are a few gotcha's with
BeginInvokethough.- If the function you are calling via
BeginInvokeaccesses shared state (state shared between the UI thread and other threads), you are in trouble. The state might change between the time you calledBeginInvokeand 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, becauseBeginInvokedoesn'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.BeginInvokegrabs a threadpool thread and executes the passed delegate on that thread.Control.BeginInvokedoes not use a threadpool thread, it does aPostMessageto 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.BeginInvokedoesn't require every call toBeginInvoketo be matched by anEndInvoke. Of course, if you are using the return value of the method and/oroutorrefvalues, then you need to call it anyway.
--
Happy day, happy life!
No comments:
Post a Comment