What's up with BeginInvoke?

本文深入探讨了在.NET应用程序中进行跨线程UI更新的方法,重点介绍了如何使用BeginInvoke来避免因不当调用导致的问题,如死锁、UI更新错误等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.

资源下载链接为: https://pan.quark.cn/s/9648a1f24758 这个HTML文件是一个专门设计的网页,适合在告白或纪念日这样的特殊时刻送给女朋友,给她带来惊喜。它通过HTML技术,将普通文字转化为富有情感和创意的表达方式,让数字媒体也能传递深情。HTML(HyperText Markup Language)是构建网页的基础语言,通过标签描述网页结构和内容,让浏览器正确展示页面。在这个特效网页中,开发者可能使用了HTML5的新特性,比如音频、视频、Canvas画布或WebGL图形,来提升视觉效果和交互体验。 原本这个文件可能是基于ASP.NET技术构建的,其扩展名是“.aspx”。ASP.NET是微软开发的一个服务器端Web应用程序框架,支持多种编程语言(如C#或VB.NET)来编写动态网页。但为了在本地直接运行,不依赖服务器,开发者将其转换为纯静态的HTML格式,只需浏览器即可打开查看。 在使用这个HTML特效页时,建议使用Internet Explorer(IE)浏览器,因为一些老的或特定的网页特效可能只在IE上表现正常,尤其是那些依赖ActiveX控件或IE特有功能的页面。不过,由于IE逐渐被淘汰,现代网页可能不再对其进行优化,因此在其他现代浏览器上运行可能会出现问题。 压缩包内的文件“yangyisen0713-7561403-biaobai(html版本)_1598430618”是经过压缩的HTML文件,可能包含图片、CSS样式表和JavaScript脚本等资源。用户需要先解压,然后在浏览器中打开HTML文件,就能看到预设的告白或纪念日特效。 这个项目展示了HTML作为动态和互动内容载体的强大能力,也提醒我们,尽管技术在进步,但有时复古的方式(如使用IE浏览器)仍能唤起怀旧之情。在准备类似的个性化礼物时,掌握基本的HTML和网页制作技巧非常
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值