What's up with BeginInvoke?

Introduction

You are developing this shiny new multithreaded .NET application that has this spanking new UI. You are done coding and it's time for giving a demo to your boss. And while running the demo, the UI hangs. Just hangs. Damn it, you say, it was working fine on my PC. You go back to your source and try to figure out why that happened. After a lot of poking around/debugging/swearing, you notice that you call this simple property on a control and it never returns. Or sometimes, it returns, but doesn't update the UI correctly. You then read something about calling BeginInvoke. What's that, you wonder. This article will try to answer that. Note that this article will be dealing with the BeginInvokemethod on the System.Windows.Forms.Control and not the BeginInvoke method that can be called on delegates to run them asynchronously.

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. Why do I need to do that, you ask. To understand why, you have to know about how Windows processes UI actions.

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. It can be thought of as:

    Message message = null;
    while ((message = GetNextMessage()) != null)
    {
        ProcessMessage(message);
    }
}

Where does the message pump run? On the Main thread, of course. So a typical GUI application, after doing some initialization in the Main method, will then start running the message pump. The while loop will exit only when the application closes, so that means the Main thread can't do anything else once the message pump starts. In .NET GUI applications, the Application.Run method takes care of message pumping. You'll see that every .NET GUI application's Main method looks like this:

public static void Main(string[] args)
{
   Form f = new Form();
   Application.Run(f);
}

That fits in nicely with our theory, Application.Run will run the message loop on the thread on which it is called. In our case, it is the main thread and you can verify that the thread doesn't get past Application.Run until the application closes by running it under the debugger. As an aside, modal dialogs (shown using Form.ShowDialog) have their own message pump whereas modeless dialogs (shown using Form.Show) don't. Which means that you can call Form.ShowDialog from any thread but Form.Show requires the calling thread to be running a message pump. Note that if you call Form.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).

Let's go deeper into the Win32 world. There are two fundamental Win32 API calls to access and/or modify a control,SendMessage and PostMessage. There is a big difference in the way the two execute. The major difference is thatSendMessage blocks the caller till the message gets processed by the message pump whereas PostMessage returns immediately. The subtle but important difference is that messages sent using SendMessage aren't queued in the message queue whereas PostMessage messages are. SendMessage messages are directly "sent" to the message pump. The message pump retrieves and processes messages sent using SendMessage before looking into those in the message queue. Effectively, there are then two queues, one for SendMessage messages and one for PostMessagemessages (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

Now it should be fairly obvious as to why the rule is around. The first reason is the blocking nature of SendMessage. Imagine a situation where the message pumping thread is waiting for your thread to complete and your thread does aSendMessageSendMessage will return only after the message pump has processed the message, but the message pump is stuck waiting for the thread to complete. Deadlock, and we know it's not exactly fun.

In .NET, property/method calls on a Control object translate to SendMessage 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.

The second reason is more subtle. Because there are effectively two queues, it's quite possible that at one point in time, both queues have the same message but with different parameters. Let's assume the message sets the text of a control and that you've used SendMessage. The message pump first processes SendMessage messages and the text is set to a particular value. It next starts processing messages in the message queue (sent using PostMessage). Along comes the same message from the message queue and it overwrites the previous value with its parameters. You never know when the message from the message queue was posted, it's very much possible that it was posted before you issued SendMessage. In that case, your most recent update gets overwritten with an older one. If the SendMessagemessage was to read the text of the control, it'll return a wrong value because the PostMessage message that executes later will set it to something else and that's what will be visible in the UI.

If instead of SendMessage, you had used PostMessage, then both messages would have gone to the same queue and barring race conditions as to who posted first to the queue, the order would have been preserved.

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 using SendMessage or one of the functions that send nonqueued[^] messages. This means that your code must be prepared to handle incoming messages when blocked on a SendMessage call. This can cause problems if your code depends on SendMessage to be a truly blocking function and doesn't use any other synchronization mechanism. For e.g., consider this piece of code:

void ButtonClick_Handler(object sender, EventArgs e)
{
   int val = count; // count is a class member variable
   SendMessage(...);
   Console.WriteLine(val == count); // Surprise, val != count can happen!
}

void SomeOtherMessageHandler()
{
   count++;
}

When the above piece of code is blocking on the SendMessage and someone else does a SendMessage to this application that causes SomeOtherMessageHandler to execute, then val will not be equal to count. The core of the issue is that SomeOtherMessageHandler will execute even if ButtonClick_Handler is blocked on SendMessage, which is not what most people expect. 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.

If you're still not convinced that SendMessage from a different thread is not a great idea, see this post for a great practical example.

Why and when to call BeginInvoke

With all that stuff inside our head now, we can easily figure out the reason for the existence of BeginInvoke. It essentially does a PostMessage. 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: InvokeBeginInvokeEndInvoke, andCreateGraphics. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread". One of the invoke methods, you say, what are the others? Well, the Control class provides one property and two methods to do the stuff we discussed.

  • InvokeRequired: This bool property returns true if the thread on which this property is called is not the thread that created this control. Basically, if InvokeRequired returns true, you need to call one of the two Invoke methods.
  • BeginInvoke: This is a functionally similar to the PostMessage 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 the BeginInvoke method on any delegate. And you can use IAsyncResult to wait for the message to be processed, just as usual. And you can call EndInvoke to get return values or outparameter values, as usual.
  • Invoke: This is like the SendMessage API function in that it waits till the message gets processed, but it doesnot do a SendMessage internally, it also does a PostMessage. 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 with SendMessage won't happen. Invoke returns the value that the function wrapped by the delegate returned.

A typical piece of code using BeginInvoke will look like this:

public class FormFoo : Form
{
   Label label = new Label();
   public static void Main()
   {
       Application.Run(new FormFoo());
   }

   public FormFoo()
   {
      InitializeComponent();

      Thread t = new Thread(new ThreadStart(ChangeLabel));
      t.Start();
   }

   private void ChangeLabel()
   {
       for (int i = 0; i<100; ++i)
       {
          SetLabelText(i);
          Thread.Sleep(1000);
       }
   }
   private delegate void SetLabelTextDelegate(int number);
   private void SetLabelText(int number)
   {
      // label.Text = number.ToString();
      // Do NOT do this, as we are on a different thread.

      // Check if we need to call BeginInvoke.
      if (this.InvokeRequired)
      {
         // Pass the same function to BeginInvoke,
         // but the call would come on the correct
         // thread and InvokeRequired will be false.
         this.BeginInvoke(new SetLabelTextDelegate(SetLabelText), 
                                          new object[] {number});

         return;
      }

      label.Text = number.ToString();
   }
}

Assigning a value to label.Text directly in SetLabelText will result in a SendMessage to the underlying control from the current thread and we saw in excruciating detail why it is troublesome. So we use BeginInvoke, passing the same function as the delegate parameter, which will post a message to the handle of this. When the message pump dispatches the message, it'll call SetLabelText and in that invocation, InvokeRequired will return false. We then set the Label's text, confident that we are on the right thread.

The BCL provides a MethodInvoker delegate, which you can use if your wrapped function takes no parameters and returns void. You can also reuse the EventHandler 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 and Invoke 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 the PostMessage thing. Apart from performance benefits, it also prevents deadlock if you call Invoke from within a method already running on the UI thread.

Invoke and BeginInvoke

Which function to use, you ask. It really depends on your requirement. If you want your UI update to complete before proceeding, you use Invoke. If there is no such requirement, I'd suggest using BeginInvoke, as it makes the thread calling it seemingly "faster". 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 called BeginInvoke 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 to BeginInvoke, which avoids the problem altogether.

Note that the above points are valid for any function that you run as a thread. They're not so obvious when usingBeginInvoke, because BeginInvoke 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.

If your code is based on .NET 1.1 (including SP1), there is a bug in the framework implementation of Invoke which could cause it to hang indefinitely on multiprocessor machines. The bug has been fixed in .NET 2.0 and as this KB article says, there is a hotfix available for 1.1, but it can only be obtained by calling PSS. You could always call BeginInvokeand use a custom signalling mechanism instead. Here is the description of the actual problem, in case you're interested.

A warning

Control.BeginInvoke, which is what we have been discussing so far, works slightly differently fromDelegate.BeginInvokeDelegate.BeginInvoke grabs a threadpool thread and executes the passed delegate on that thread. Control.BeginInvoke does not use a threadpool thread, it does a PostMessage 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.BeginInvokeControl.BeginInvoke doesn't require every call to BeginInvoke to be matched by an EndInvoke. Of course, if you are using the return value of the method and/or out or ref values, then you need to call it anyway.

Conclusion

Judging from the number of posts in the C# forum resulting from the improper use of BeginInvoke, this seems to be a poorly understood topic. Hopefully this article will help clarify things a bit. In .NET 2.0, the CLR straightaway throws an exception if you attempt to do a cross thread UI update. In fact, I'd suggest compiling your app for the 2.0 platform and running the app through all possible scenarios, just to make sure there are no inadvertent cross thread UI updates.

Appendix

Note 1 Not entirely true, as explained later in the article. Some of the messages are posted to the message queue (PostMessage) and some are directly sent to the message pump (SendMessage). Sent messages are processed first before processing messages in the message queue.

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值