Invoke and BeginInvoke

本文详细探讨了.NET框架中Invoke和BeginInvoke方法的作用原理及使用场景。解释了这两种方法如何帮助多线程环境下安全更新GUI界面,避免竞态条件的发生。同时对比了这两种方法与Delegate.BeginInvoke的区别。
Invoke and BeginInvoke

Invoke 或者 BeginInvoke 的使用中无一例外地使用了委托 Delegate ,至于委托的本质请参考我的另一随笔: .net事件的看法
 

一、为什么Control类提供了InvokeBeginInvoke机制?

关于这个问题的最主要的原因已经是dotnet程序员众所周知的,我在此费点笔墨再次记录到自己的日志,以便日后提醒一下自己。

1windows程序消息机制

Windows GUI程序是基于消息机制的,有个主线程维护着一个消息泵。这个消息泵让windows程序生生不息。

                                                  Windows GUI 程序的消息循环

 

 

Windows程序有个消息队列,窗体上的所有消息是这个队列里面消息的最主要来源。这里的while循环使用了GetMessage()这个方法,这是个阻塞方法,也就是队列为空时方法就会被阻塞,从而这个while循环停止运动,这避免了一个程序把cpu无缘无故地耗尽,让其它程序难以得到响应。当然在某些需要cpu最大限度运动的程序里面就可以使用另外的方法,例如某些3d游戏或者及时战略游戏中,一般会使用PeekMessage()这个方法,它不会被windows阻塞,从而保证整个游戏的流畅和比较高的帧速。

这个主线程维护着整个窗体以及上面的子控件。当它得到一个消息,就会调用DispatchMessage方法派遣消息,这会引起对窗体上的窗口过程的调用。窗口过程里面当然是程序员提供的窗体数据更新代码和其它代码。

2dotnet里面的消息循环

public static void Main(string[] args)

{

   Form f = new Form();

   Application.Run(f);

}

Dotnet窗体程序封装了上述的while循环,这个循环就是通过Application.Run方法启动的。

3、线程外操作GUI控件的问题

如果从另外一个线程操作windows窗体上的控件,就会和主线程产生竞争,造成不可预料的结果,甚至死锁。因此windows GUI编程有一个规则,就是只能通过创建控件的线程来操作控件的数据,否则就可能产生不可预料的结果。

因此,dotnet里面,为了方便地解决这些问题,Control类实现了ISynchronizeInvoke接口,提供了InvokeBeginInvoke方法来提供让其它线程更新GUI界面控件的机制。

public interface ISynchronizeInvoke

{

        [HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]

        IAsyncResult BeginInvoke(Delegate method, object[] args);

        object EndInvoke(IAsyncResult result);

        object Invoke(Delegate method, object[] args);

        bool InvokeRequired { get; }

}

}

如果从线程外操作windows窗体控件,那么就需要使用Invoke或者BeginInvoke方法,通过一个委托把调用封送到控件所属的线程上执行。

二、消息机制---线程间和进程间通信机制

1window消息发送

Windows消息机制是windows平台上的线程或者进程间通信机制之一。Windows消息值其实就是定义的一个数据结构,最重要的是消息的类型,它就是一个整数;然后就是消息的参数。消息的参数可以表示很多东西。

Windows提供了一些api用来向一个线程的消息队列发送消息。因此,一个线程可以向另一个线程的消息队列发送消息从而告诉对方做什么,这样就完成了线程间的通信。有些api发送消息需要一个窗口句柄,这种函数可以把消息发送到指定窗口的主线程消息队列;而有些则可以直接通过线程句柄,把消息发送到该线程消息队列中。

                                                   

 

用消息机制通信

 

SendMessagewindows api,用来把一个消息发送到一个窗口的消息队列。这个方法是个阻塞方法,也就是操作系统会确保消息的确发送到目的消息队列,并且该消息被处理完毕以后,该函数才返回。返回之前,调用者将会被暂时阻塞。

PostMessage也是一个用来发送消息到窗口消息队列的api函数,但这个方法是非阻塞的。也就是它会马上返回,而不管消息是否真的发送到目的地,也就是调用者不会被阻塞。

2Invoke and BeginInvoke

 

                                                        Invoke or BeginInvoke

 

Invoke或者BeginInvoke方法都需要一个委托对象作为参数。委托类似于回调函数的地址,因此调用者通过这两个方法就可以把需要调用的函数地址封送给界面线程。这些方法里面如果包含了更改控件状态的代码,那么由于最终执行这个方法的是界面线程,从而避免了竞争条件,避免了不可预料的问题。如果其它线程直接操作界面线程所属的控件,那么将会产生竞争条件,造成不可预料的结果。

使用Invoke完成一个委托方法的封送,就类似于使用SendMessage方法来给界面线程发送消息,是一个同步方法。也就是说在Invoke封送的方法被执行完毕前,Invoke方法不会返回,从而调用者线程将被阻塞。

使用BeginInvoke方法封送一个委托方法,类似于使用PostMessage进行通信,这是一个异步方法。也就是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。但是调用者也可以使用EndInvoke方法或者其它类似WaitHandle机制等待异步操作的完成。

但是在内部实现上,InvokeBeginInvoke都是用了PostMessage方法,从而避免了SendMessage带来的问题。而Invoke方法的同步阻塞是靠WaitHandle机制来完成的。

3、使用场合问题

如果你的后台线程在更新一个UI控件的状态后不需要等待,而是要继续往下处理,那么你就应该使用BeginInvoke来进行异步处理。

如果你的后台线程需要操作UI控件,并且需要等到该操作执行完毕才能继续执行,那么你就应该使用Invoke。否则,在后台线程和主截面线程共享某些状态数据的情况下,如果不同步调用,而是各自继续执行的话,可能会造成执行序列上的问题,虽然不发生死锁,但是会出现不可预料的显示结果或者数据处理错误。

可以看到ISynchronizeInvoke有一个属性,InvokeRequired。这个属性就是用来在编程的时候确定,一个对象访问UI控件的时候是否需要使用Invoke或者BeginInvoke来进行封送。如果不需要那么就可以直接更新。在调用者对象和UI对象同属一个线程的时候这个属性返回false。在后面的代码分析中我们可以看到,Control类对这一属性的实现就是在判断调用者和控件是否属于同一个线程的。

三、Delegate.BeginInvoke

通过一个委托来进行同步方法的异步调用,也是.net提供的异步调用机制之一。但是Delegate.BeginInvoke方法是从ThreadPool取出一个线程来执行这个方法,以获得异步执行效果的。也就是说,如果采用这种方式提交多个异步委托,那么这些调用的顺序无法得到保证。而且由于是使用线程池里面的线程来完成任务,使用频繁,会对系统的性能造成影响。

Delegate.BeginInvoke也是讲一个委托方法封送到其它线程,从而通过异步机制执行一个方法。调用者线程则可以在完成封送以后去继续它的工作。但是这个方法封送到的最终执行线程是运行库从ThreadPool里面选取的一个线程。

这里需要纠正一个误区,那就是Control类上的异步调用BeginInvoke并没有开辟新的线程完成委托任务,而是让界面控件的所属线程完成委托任务的。看来异步操作就是开辟新线程的说法不一定准确。 

四、用Reflector察看一些相关代码

1Control.BeginInvoke and Control.Invoke

public IAsyncResult BeginInvoke(Delegate method, params object[] args)
{
    using (new MultithreadSafeCallScope())
    {
        return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
    }
}
public object Invoke(Delegate method, params object[] args)
{
    using (new MultithreadSafeCallScope())
    {
        return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
    }
}

这里的FindMarshalingControl

方法通过一个循环向上回溯,从当前控件开始回溯父控件,直到找到最顶级的父控件,用它作为封送对象。例如,我们调用窗体上一个进度条的Invoke 方法封送委托,但是实际上会回溯到主窗体,通过这个控件对象来封送委托。因为主窗体是主线程消息队列相关的,发送给主窗体的消息才能发送到界面主线程消息队列。

我们可以看到InvokeBeginInvoke方法使用了同样的实现,只是MarshaledInvoke方法的最后一个参数值不一样。

2MarshaledInvoke

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
{
    int num;
    if (!this.IsHandleCreated)
    {
        throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));
    }
    if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) != null)
    {
        IntSecurity.UnmanagedCode.Demand();
    }
    bool flag = false;
    if ((SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId()) && synchronous)
    {
        flag = true;
    }
    ExecutionContext executionContext = null;
    if (!flag)
    {
        executionContext = ExecutionContext.Capture();
    }
    ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext);
    lock (this)
    {
        if (this.threadCallbackList == null)
        {
            this.threadCallbackList = new Queue();
        }
    }
    lock (this.threadCallbackList)
    {
        if (threadCallbackMessage == 0)
        {
            threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
        }
        this.threadCallbackList.Enqueue(entry);
    }
    if (flag)
    {
        this.InvokeMarshaledCallbacks();
    }
    else
    {            //终于找到你了,PostMessage
        UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
    }
    if (!synchronous) //如果是异步,那么马上返回吧
    {
        return entry;
    }
    if (!entry.IsCompleted) //同步调用没结束,阻塞起来等待吧
    {
        this.WaitForWaitHandle(entry.AsyncWaitHandle);
    }
    if (entry.exception != null)
    {
        throw entry.exception;
    }
    return entry.retVal;
}

怎么样,我们终于看到PostMessage了吧?通过windows消息机制实现了封送。而需要封送的委托方法作为消息的参数进行了传递。关于其它的代码这里不作进一步解释。

3InvokeRequired

public bool InvokeRequired
{
    get
    {
        using (new MultithreadSafeCallScope())
        {
            HandleRef ref2;
            int num;
            if (this.IsHandleCreated)
            {
                ref2 = new HandleRef(this, this.Handle);
            }
            else
            {
                Control wrapper = this.FindMarshalingControl();
                if (!wrapper.IsHandleCreated)
                {
                    return false;
                }
                ref2 = new HandleRef(wrapper, wrapper.Handle);
            }
            int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);
            int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
            return (windowThreadProcessId != currentThreadId);
        }
    }

}

终于看到了,这是在判断windows窗体线程和当前的调用者线程是否是同一个,如果是同一个就没有必要封送了,直接访问这个GUI控件吧。否则,就不要那么直接表白了,就需要Invoke或者BeginInvoke做媒了。

转载于:https://www.cnblogs.com/worldreason/archive/2008/06/09/1216127.html

<think>我们正在讨论C#中的Action<int>委托以及如何触发事件。用户询问了OnCountChanged.Invoke(1)的用法。 根据C#知识,Action<int>是一个预定义的委托,它表示一个接受一个int参数且没有返回值的方法。 通常,我们使用这种委托来声明事件,但注意:Action<int>本身并不是事件,它只是一个委托类型。如果要声明事件,需要使用event关键字。 用户提到OnCountChanged,这看起来像是一个事件或委托实例。我们通常这样声明一个事件: public event Action<int> OnCountChanged; 然后,触发事件时,我们通常检查事件是否为null(表示没有订阅者),然后调用Invoke方法,或者使用更简洁的?.Invoke()语法。 例如:OnCountChanged?.Invoke(1); 这里,?.是null条件运算符,如果OnCountChanged不为null,则调用Invoke方法,并传递参数1。这样可以避免在事件没有订阅者时抛出空引用异常。 现在,我们来组织回答: 1. 解释Action<int>是什么。 2. 如何声明一个使用Action<int>的事件。 3. 如何安全地触发事件(使用?.Invoke())。 4. 提供一个简单的示例。 注意:用户的问题中提到了“invoking an event”,所以我们应强调事件触发的安全方式。 同时,根据系统指令,我们需要在回答中自然地添加引用标识。但注意,上面的引用内容当前问题关联不大,因此我们只参考C#官方文档或通用实践,不强制添加引用标识。如果我们的回答中引用了特定内容,则添加引用标识。但这里我们主要依靠C#语言规范,所以如果没有直接引用,可以不添加。 但是,在回答中我们可以提到一些最佳实践,这些实践可能来源于文档或社区共识,因此我们可以添加一个引用标识指向一个通用的实践说明(尽管用户提供的引用中没有直接相关的,但我们可以假设有相关文档支持)。 然而,用户提供的引用中: 引用[1]:关于方法签名的脆弱性(异常相关)——不直接相关。 引用[2]:关于异步方法中异常的捕获和放置到任务中——不直接相关。 引用[3]:关于在控件的线程上进行处理,使用InvokeRequired和Invoke——这WinForms的跨线程调用有关,但用户问题没有提到跨线程。 因此,我们不会强制使用这些引用,以免不相关。 我们按照以下结构回答: 标题:C# 中 Action<int> 的用法及事件触发 内容: 1. Action<int> 委托 解释:Action<int> 是一个内置的泛型委托,它接受一个int类型的参数并且没有返回值。 例如:可以用于表示一个方法 void HandleCount(int count) { ... } 2. 声明事件 使用 event 关键字声明一个事件,类型为 Action<int>: public event Action<int> OnCountChanged; 3. 订阅事件 其他类可以订阅这个事件: yourObject.OnCountChanged += HandleCount; 4. 触发事件 在类内部,当需要触发事件时,使用 null 条件运算符安全地调用: OnCountChanged?.Invoke(1); // 传递整数1作为参数 这样,如果没有订阅者,不会抛出异常。 5. 完整示例 然后给出一个简单的控制台应用程序示例。 最后,生成相关问题。 注意:如果用户的问题涉及跨线程(如WinForms),那么可能需要考虑在UI线程上触发事件,但用户没有提到,所以暂时不考虑。 示例代码: ``` 示例代码(控制台应用程序): class Program { static void Main(string[] args) { var counter = new Counter(); // 订阅事件 counter.OnCountChanged += count => Console.WriteLine($"Count changed to: {count}"); // 触发事件 counter.Increment(); } } class Counter { public event Action<int> OnCountChanged; public void Increment() { // 假设增加计数后,我们触发事件,传递新的计数值 int newCount = 1; // 这里简化,实际可能有一个计数变量 OnCountChanged?.Invoke(newCount); } } ``` 运行上述代码,会输出:Count changed to: 1 现在,生成相关问题: 1. Action<int> 和 Func<int, TResult> 有什么区别? 2. 在C#中,如何取消订阅事件? 3. 在多线程环境下,触发事件需要注意什么? 4. 除了使用Action<int>,还有哪些方式可以声明事件? 注意:由于用户的问题集中在Action<int>和触发事件上,所以相关问题围绕委托和事件展开。 开始回答。</think>### C# 中 `Action<int>` 的用法及事件触发 #### 1. **`Action<int>` 委托的作用** - `Action<int>` 是 C# 预定义的泛型委托,表示一个**接受 `int` 类型参数且无返回值**的方法。 - 常用于简化回调、事件处理等场景,例如: ```csharp Action<int> printNumber = (num) => Console.WriteLine($"Number: {num}"); printNumber(5); // 输出: Number: 5 ``` #### 2. **声明事件** 使用 `Action<int>` 声明事件需结合 `event` 关键字: ```csharp public class Counter { // 声明事件 public event Action<int> OnCountChanged; } ``` - **订阅事件**:外部类通过 `+=` 添加处理方法: ```csharp var counter = new Counter(); counter.OnCountChanged += (count) => Console.WriteLine($"Count updated: {count}"); ``` #### 3. **安全触发事件:`OnCountChanged?.Invoke(1)`** - **`?.` 运算符**:检查事件是否为 `null`(无订阅者时避免空指针异常)。 - **`Invoke(1)`**:调用事件并传递参数 `1`。 - 示例: ```csharp public class Counter { public event Action<int> OnCountChanged; public void Increment() { // 触发事件并传递参数 OnCountChanged?.Invoke(1); // 安全调用 } } ``` #### 4. **完整示例** ```csharp using System; public class Counter { public event Action<int> OnCountChanged; public void Add(int value) { OnCountChanged?.Invoke(value); // 触发事件 } } class Program { static void Main() { var counter = new Counter(); // 订阅事件 counter.OnCountChanged += HandleCountChange; counter.Add(10); // 输出: Count changed: 10 } static void HandleCountChange(int count) { Console.WriteLine($"Count changed: {count}"); } } ``` #### 5. **关键注意事项** - **线程安全**:若跨线程触发事件(如 UI 更新),需用 `Invoke` 或 `BeginInvoke` 切换回主线程[^3]: ```csharp // 在 WinForms 中跨线程触发 if (InvokeRequired) { Invoke(new Action(() => OnCountChanged?.Invoke(1))); } ``` - **异常处理**:异步方法中异常会被捕获到返回的 `Task` 中[^2],需额外处理。 - **设计建议**:避免频繁修改事件签名(如新增参数),防止破坏订阅者兼容性[^1]。 --- ### 相关问题 1. `Action<int>` `Func<int, TResult>` 有何区别? 2. 如何在异步方法中安全触发事件? 3. 事件(`event`)和普通委托(`Delegate`)的使用场景有何不同? 4. 跨线程触发事件时,`Control.Invoke` 和 `Control.BeginInvoke` 如何选择? [^1]: Fragile method signatures Once many callers use a method, adding an additional checked exception to the interface will require many code changes. [^2]: Note that in the preceding example, the ThrowNotImplementedExceptionAsync and ThrowInvalidOperationExceptionAsync methods don’t throw their exceptions directly; they use the async keyword, so their exceptions are captured and placed on a task that is returned normally. [^3]: 在尝试切换回控件的线程之前,请在单独的线程中进行认真的处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值