`
北极的。鱼
  • 浏览: 159248 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

【转】C# Control的Invoke和BeginInvoke及其实现机制

阅读更多

转自:

http://www.soft-bin.com/html/2010/07/09/c-control%e7%9a%84invoke%e5%92%8cbegininvoke%e5%8f%8a%e5%85%b6%e5%ae%9e%e7%8e%b0%e6%9c%ba%e5%88%b6.html

 

与C++不同,C#语言禁止在创建某个控件的线程外对控件进行访问,否则会引起访问违规的异常。但有些时候,我们的确需要从其他线程对控件,此时就需要借助于Invoke和BeginInvoke之手了。我们先来对Invoke进行介绍。

Invoke是控件Control的成员方法,函数有两个重载版本:

Invoke(Delegate) //在拥有此控件的基础窗口句柄的线程上执行指定的委托。
Invoke(Delegate, object[]) //在拥有控件的基础窗口句柄的线程上,用指定的参数列表执行指定委托。

第一个版本用于Invoke不带参数的委托,第二个版本用于Invoke带参数的委托,没有实质的区别。

Invoke 的作用MSDN上描述得有些晦涩,我会尝试用简单点的语言来进行表述。Invoke的作用是,将一个Delegate代表的函数(Delegate可以理解为函数指针)移交到控件所在线程中执行,且这个操作是同步操作,在Delegate代表的函数执行完毕后,Invoke函数才会返回。


当我们需要在其他线程总对一个控件进行操作时,可以如下操作:

delegate void Delegate0();

// 假设form是 FuncOnControl函数要使用到的form
public this Form1 form;
private void FuncOnControl()
{
      Control ctrl  = (Control)this.form;
      if (ctrl.InvokeRequired)
      {
            Delegate0 d = new Delegate0(FuncOnControl);
            ctrl.Invoke(d);
            return;
      }

      // 真正的 FuncOnControl 应当进行的操作
}

上面代码中用到 Control.InvokeRequired这个属性,这个属性表示如果要对Control进行操作,是否需要使用到Invoke来进行线程切换。这个属性的判断依据即当前线程和创建Control的线程是否是同一个线程。

在执行了Invoke以后,不要忘记了return,因为整个函数将会在另外一个线程中被运行,如果没有return,则函数的本次调用会继续下去,导致函数被运行两次,且仍然在当前线程中访问Control。

Invoke 是如何将函数送到其他线程去执行的呢?这里我首先想到的是消息循环,向创建Control的线程发送一个消息,在消息处理函数中调用Delegate代表的函数,这样是肯定可以实现线程切换的。

我们可以编写一个程序来验证Invoke是否真的是使用消息循环机制实现,这个程序将分析控件所在线程收到的消息,验证在Invoke调用的时候,该线程是否收到了可疑的消息。

首先创建一个C#的windows application。

我需要一个跟消息循环相关的类,以发送,接收消息,于是我创建一个类,名为WinApi.cs,其源码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace Test
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct MSG
    {
        public IntPtr hwnd;
        public uint message;
        public uint wParam;
        public uint lParam;
        public uint time;
        public POINT pt;
    }

    public class WinApi
    {
        [DllImport("user32.dll")]
        public static extern void PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern bool GetMessage(ref MSG msg, IntPtr hWnd, int min, int max);

        [DllImport("user32.dll")]
        public static extern void TranslateMessage(ref MSG msg);

        [DllImport("user32.dll")]
        public static extern void DispatchMessage(ref MSG msg);
    }
}

上面几个函数的功能我在 Windows消息机制里都已经讲过了,在这里我只做简要说明,如果有什么疑问,请参考那一篇文章。

MSG 是Windows消息机制中最重要的一个结构体,表示一个消息。
PostMessage 函数向某一个窗口发送一个指定的消息。
GetMessage 从消息队列中获取消息,其参数hWnd表示要获取发送给哪一个窗口的消息,如果为NULL,则表示所有消息。
TranslateMessage 将消息进行翻译,主要是处理键盘输入相关的东西。
DispatchMessage 对消息进行分发,根据消息的hWnd调用不同的窗口过程。

有了准备工作以后,我需要两个Windows窗口,暂且命名为Form1和Form2。其中Form1为主窗体,Form2为第二个窗体,这个Form2需要在第二个线程中运行,以便我可以点击Form2的按钮,然后调用Form1的Invoke函数,实现线程的切换。看起来有点复杂? 那就一步一步来做吧。

在创建工程的时候,IDE会自动为我们创建一个Form1,那么我们现在先添加一个From,使用其默认的名称Form2。在Form2里添加一个Form1类型的public成员,如下:

public Form1 form1 = null;

我们还需要一个函数,用来Invoke:

void Func(object sender, EventArgs arg)
{
    int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
    MessageBox.Show("Function Thread:" + id);
}

再给Form2添加一个按钮,以执行Invoke操作:

private void BtnInvoke_Click(object sender, EventArgs e)
{
    MessageBox.Show("Invoker Thread:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
    EventHandler d = new EventHandler(Func);

    Control ctrl = (Control)this.form1;
    ctrl.Invoke(d);
}

Form2已经就绪了,下面给Form1增加一个按钮,以启动Form2,这个按钮的响应代码为:

private void button1_Click(object sender, EventArgs e)
{
    Thread t = new Thread(new ThreadStart(this.ThreadFunc));
    t.Start();
}

当然,不要忘记了 ThreadFunc 的定义,以及在文件头部加上对 System.Thread 名字空间的引入

private void ThreadFunc()
{
    Form2 form2 = new Form2();
    form2.form1 = this;
    form2.ShowDialog();
}

还记得么?在Start a C# application中提到的, ShowDialog 会让form2正常运行起来。

想想看,Form2有了,可以Invoke了,Form1中也增加了启动Form2的接口了,那么我们下面该验证Invoke是否是向Form1所在线程发了消息了。我们可以直接重载Form1的WndProc以截获消息,但又考虑到消息可能是直接发送到线程中去了,hWnd并不是Form1的句柄,因此我们要做得更绝一点:启动一个新的消息循环!

给Form1添加一个按钮,在其响应代码中输入如下代码:

private void BtnStartMsgLoop_Click(object sender, EventArgs e)
{
    MSG msg = new MSG();
    while (WinApi.GetMessage(ref msg, IntPtr.Zero, 0, 0))
    {
        MessageBox.show("msg recved:" + msg.message);

        WinApi.TranslateMessage(ref msg);
        WinApi.DispatchMessage(ref msg);
    }
}

我们点击这个按钮以后,消息循环就被我们改变了,是不是很神奇? 我们可以把所有的消息都用MessageBox弹出来查看,看看Invoke是不是用到了消息。但实际上这段代码一运行,我们就会发现这实在是一个很烂的点子,运行过程中Form1接收到了太多的无关消息了,要是能过滤这些无关消息就好了。可以做到么? 可以!

给Form1添加一个成员:

public List<int> MsgList = new List<int>();

MsgList将保存所有的与Invoke无关的可能出现的消息,下面我们将需要向MsgList中填入数据。

添加一个按钮,响应代码如下:

private void BtnInitMsgList_Click(object sender, EventArgs e)
{
    MSG msg = new MSG();
    while (WinApi.GetMessage(ref msg, IntPtr.Zero, 0, 0))
    {
        if (MsgList.Contains((int)msg.message) == false)
        {
            MsgList.Add((int)msg.message);
        }

        // exit message loop
        if (msg.message == 110100)
        {
            break;
        }

        if (msg.hwnd == IntPtr.Zero)
        {
             //MessageBox.Show("Msg Recved:" + msg.message);
             uint message = msg.message;
              continue;
        }

                WinApi.TranslateMessage(ref msg);
                WinApi.DispatchMessage(ref msg);
    }
}

没错,我们又建立了一个消息循环,不过这个消息循环是可以退出的,当接收到的消息为110100时,这个消息循环就会退出,返回到系统的主消息循环中。

好吧,我么又得需要一个按钮了,这个按钮用于退出这个消息循环:

private void EndInitMsgList_Click(object sender, EventArgs e)
{
    WinApi.PostMessage(this.Handle, 110100, IntPtr.Zero, IntPtr.Zero);
}

既然有了MsgList,那么BtnStartMsgLoop_Click函数也该做相应修改了:

private void BtnStartMsgLoop_Click(object sender, EventArgs e)
{
    MSG msg = new MSG();
    while (WinApi.GetMessage(ref msg, IntPtr.Zero, 0, 0))
    {
        if (MsgList.Contains((int)msg.message) == false)
        {
            uint m = msg.message;
        }

        WinApi.TranslateMessage(ref msg);
        WinApi.DispatchMessage(ref msg);
    }
}

我把之前的MessageBox.Show替换成了 uint m = msg.message; 这是有原因的,还记得么? 模态对话框会构建自己的消息循环,我不大想让自己好不容易夺取来的消息循环控制权又转让给了MessageBox,所以我尽量不去使用它,我宁愿在这里加一个断点来进行调试。所以,请本文的读者在 uint m = msg.message 处加设一个断点吧。

是不是有点头晕了? 下面我来整理下这些Form以及其上的按钮:
Form1:
BtnOpenForm2 : 在一个线程中启动form2
BtnInitMsgList : 启动初始化MsgList的消息循环,将所有出现的消息记录在MsgList中
BtnEndInitMsgList : 给自身发送1101,结束初始化MsgList的过程
BtnStartMsgLoop : 启动消息循环,并对消息进行检测。记住,别忘了在 uint m = msg.message 处加一个断点

Form2:
这个就简单了,只有一个调用Invoke的按键

现在开始测试:
Step 1: 启动调试,会弹出Form1
Step 2: 在Form1 上点击BtnInitMsgList,第大概3秒钟,在这个过程中移动Form1的界面,最大最小化Form1,尽量让消息都被保存到MsgList中。
Step 3: 在Form1 上点击BtnEndInitMsgList,等大概1秒钟
Step 4: 在Form1 上点击BtnOpenForm2,则Form2 启动
Step 5: 在Form1 上点击BtnStartMsgLoop,将消息循环转移到自己的控制下
Step 6: 点击Form2 上的Invoke按钮,调用Form1.Invoke

我们可以很明显地看到,Form2 调用Invoke以后,我们在Form1中设置的断点立马有反应了,跟踪调试,就会发现DispatchMessage的下一行就是Form2中的Func。

在这个程序的基础上,大家可以任意发挥,做自己想做的测试,但我从这个测试里,只得出结论,Invoke实际上是利用消息循环来实现的,但我并不知道Invoke向消息循环中发出了什么参数,实际上 lParam和wParam都是0。如果你得出的lParam和wParam不是0,那么很抱歉,可能那个消息并不是我们需要截获的那一个,你需要MsgList保存更多的无关消息。

我不知道Invoke的参数,也就是要执行的函数和它的参数,是如何传递的,如果有谁知道的话,希望能过告诉我,我的联系方式是 luckzj12@163.com ^_^

到这里,Invoke可以告一段落,下面该是BeginInvoke了。BeginInvoke实现的功能和Invoke一样,都是实现线程切换,只是不同的是,BeginInvoke 抛送消息后,立即返回,而不会阻塞等待执行结束,也就是说,这是一个异步的操作,其原型为:

public IAsyncResult BeginInvoke(
	Delegate method
)

public IAsyncResult BeginInvoke(
	Delegate method,
	Object[] args
)

一切都跟Invoke的一样,区别就是Invoke返回是object,即Invoke所调用的函数的返回值,而BeginInvoke返回的是 IAsyncResult。

IAsyncResult 实际上相当于一个证明,证明你调用了这个BeginInvoke,你如果想知道调用的结果,就可以调用EndInvoke来获取:

public Object EndInvoke(
	IAsyncResult asyncResult
)

EndInvoke通常和BeginInvoke配合使用,以获取BeginInvoke调用的函数的返回值,如果调用EndInvoke时,操作尚未完成,则EndInvoke就会一直等待。

这就好比说是你要出门履行,将行礼托运,托运的时候对方会给你开一个托运的单子,等你到了目的地,想取回行李的时候,你就可以拿出这个单子,找托运方换回你的行李,当然,如果行李还在路上,那么不好意思,你得老老实实在那等着了,直到行李来了,你交回单据,才能离开。

BeginInvoke与Invoke相比,更加类似于PostMessage,我们同样可以使用上面的测试方法,来对BeginInvoke的实现原理进行测试,所得出的结论也是一样的: 我们的确收到了消息,但我们却无法分析出更多的实现细节。

 

 

 

 

参考博文:

1:Windows消息机制
http://www.soft-bin.com/html/2010/07/07/windows%e6%b6%88%e6%81%af%e6%9c%ba%e5%88%b6.html

2: C#中调用Win API函数
http://www.soft-bin.com/html/2010/07/15/csharpcallwinapi.html

3: Start a c# application
http://www.soft-bin.com/html/2010/07/08/start-a-c-application.html

分享到:
评论

相关推荐

    C#窗体中Invoke和BeginInvoke方法详解

    在探讨C#窗体中`Invoke`和`BeginInvoke`方法的使用及其重要性之前,我们首先需要理解.NET框架下的多线程与GUI操作的基本原则,以及为何这两者在跨线程更新GUI时不可或缺。 #### 一、为什么Control类提供了Invoke和...

    Invoke 与BeginInvoke的区别

    在.NET框架中,`Invoke` 和 `BeginInvoke` 是两个常用的方法,主要用于实现跨线程访问控件或执行操作。这两种方法通常出现在多线程编程场景中,特别是当涉及到UI线程与其他线程之间的交互时。 **`Control.Invoke` ...

    c# Invoke和BeginInvoke 区别分析

    在C#编程中,`Invoke` 和 `BeginInvoke` 是两个关键的方法,它们主要用于处理多线程环境下的UI更新问题。这两个方法都是Windows Forms或WPF应用程序中`Control`类的成员,它们允许非UI线程对UI组件进行操作,因为...

    c#中委托,事件和BeginInvoke在进程间传递消息的作用

    委托、事件和BeginInvoke在C#中是实现进程间消息传递的重要概念和工具。委托可以理解为一个可以持有对具有特定参数列表和返回类型的方法的引用的类型。事件是一种特殊的委托,用于实现发布-订阅模式,其中发布者触发...

    C#Control.Invoke方法和跨线程访问控件共

    总的来说,`Control.Invoke`是C#中实现跨线程访问UI控件的关键工具,它确保了UI操作的安全性和线程合规性。正确理解和使用`Invoke`方法对于编写高效、健壮的多线程应用程序至关重要。同时,开发者还应注意在多线程...

    C# 线程访问UI 代理Invoke技术 标准实现

    总结起来,C#中通过代理和`Invoke`技术实现线程访问UI,可以确保多线程环境下的UI操作安全性。这种方法不仅适用于Windows Forms,同样适用于WPF等其他UI框架。在设计多线程应用程序时,理解并正确应用这一技术至关...

    VB.Net-C#多线程Thread-代理委托delegate编程

    Control的Invoke和BeginInvoke.txt Invoke和BeginInvoke的真正含义.txt NET异步调用模式.txt TreeView更新线程.txt url.txt VB.NET多线程——创建新线程.txt VB.NET多线程——高级同步技术.txt VB.NET多线程——...

    c#.net异步机制

    C#.NET异步机制是.NET框架中用于提升应用程序性能的关键特性,它允许代码在等待I/O操作(如网络通信或磁盘读写)完成时,不阻塞主线程执行其他任务,从而提高程序的响应性和效率。在.NET Framework 4.0及更高版本中...

    c#跨线程跨类调用窗体控件

    在C#中,我们可以使用`Control.Invoke`或`Control.BeginInvoke`方法。这两个方法都用于在UI线程上执行委托,但`Invoke`是同步的,会阻塞当前线程直到委托完成;而`BeginInvoke`是异步的,它立即返回,让UI线程在合适...

    C#多线程解决界面卡死问题的完美解决方案_极简版

    在Windows Forms中,可以使用Control.Invoke或BeginInvoke。 总结来说,C#中多线程解决界面卡死问题的方法主要包括使用BackgroundWorker和async/await模式。这两种方法都旨在将耗时操作与UI更新分离,确保用户界面...

    WinFormInvoke_winform多线程_防卡死_

    此时,我们需要使用`Control.Invoke`或`Control.BeginInvoke`方法。这两个方法会在UI线程上下文中执行指定的委托,确保了对UI的修改安全。 - `Invoke`: 同步调用,会阻塞工作线程直到UI线程完成操作。 - `...

    C#跨线程调用控件的四种方式

    1. **Control.Invoke() 和 Control.BeginInvoke()** 这是最常见的处理跨线程操作的方法。`Invoke`方法会同步地执行委托,直到完成才会返回,而`BeginInvoke`则异步执行,不阻塞调用线程。两者都需要一个代表UI线程...

    C# 线程A访问非线程A创建的控件 、 线程内创建窗体置顶显示ShowDialog

    在C#编程中,线程安全性和多线程交互是一个...通过合理利用`Invoke`、`BeginInvoke`、`Dispatcher`和`SynchronizationContext`,以及适当地处理窗体的显示和定位,可以在不干扰用户交互的情况下实现复杂的并发任务。

    C#多线程解决界面卡死问题的完美解决方案

    由于UI控件只能在创建它的线程中修改,因此在后台线程中更新UI需要使用`Control.Invoke`或`Control.BeginInvoke`。例如: ```csharp if (this.InvokeRequired) { this.Invoke(new Action(() =&gt; { // 更新UI }));...

    基于C#实现鼠标键盘事件模拟

    如果模拟操作在非UI线程执行,可能需要使用Control.Invoke或Control.BeginInvoke来确保操作在正确的上下文中执行。 最后,压缩包中的"Debug"文件可能包含编译后的可执行文件、日志文件或调试信息。这些文件对于运行...

    C#网络编程详解

    为解决这个问题,C#引入了Control类的Invoke和BeginInvoke方法。 Invoke方法是同步的,它会阻塞调用线程,直到在UI线程上执行指定的委托。这确保了对UI控件的访问是在正确的线程上下文中进行的。例如,当从网络接收...

    C#编写的钟表

    为了保证线程安全,可能需要用到`Control.Invoke`或`Control.BeginInvoke`方法来在UI线程中执行更新操作。 6. **格式化时间**: C#提供了丰富的日期和时间格式化选项,通过`DateTime.Now`获取当前时间,然后使用`...

    c#多线程中子线程动态改变ui控件

    C#子线程更新UI控件有两种常用的方法:使用控件自身的Invoke/BeginInvoke方法和使用SynchronizationContext的Post/Send方法更新方法。读者可以根据实际情况选择合适的方法,以便更好地控制UI界面。

Global site tag (gtag.js) - Google Analytics