如何使事件处理程序异步运行?

Posted

技术标签:

【中文标题】如何使事件处理程序异步运行?【英文标题】:How do I make an eventhandler run asynchronously? 【发布时间】:2010-12-27 07:54:52 【问题描述】:

我正在编写一个在辅助线程上执行连续循环操作的 Visual C# 程序。有时,当该线程完成任务时,我希望它触发事件处理程序。我的程序会这样做,但是当事件处理程序被触发时,辅助线程会等到事件处理程序完成后再继续线程。我如何让它继续?这是我目前的结构方式...

class TestClass 

  private Thread SecondaryThread;
  public event EventHandler OperationFinished;

  public void StartMethod()
  
    ...
    SecondaryThread.Start();      //start the secondary thread
  

  private void SecondaryThreadMethod()
  
    ...
    OperationFinished(null, new EventArgs());
    ...  //This is where the program waits for whatever operations take
         //place when OperationFinished is triggered.
  


此代码是我的一台设备的 API 的一部分。当 OperationFinished 事件被触发时,我希望客户端应用程序能够做它需要做的任何事情(即相应地更新 GUI)而不需要拖拽 API 操作。

另外,如果我不想将任何参数传递给事件处理程序,使用 OperationFinished(null, new EventArgs()) 的语法是否正确?

【问题讨论】:

您希望在哪个线程上引发 OperationFinished 事件?它不能是您的辅助线程,因为您明确要求不要阻止它。那么,它是否必须是主线程,或者您是否可以在仅为异步回调目的而新创建的不同线程上引发它? 【参考方案1】:

也许下面的 Method2 或 Method3 会有所帮助:)

public partial class Form1 : Form

    private Thread SecondaryThread;

    public Form1()
    
        InitializeComponent();

        OperationFinished += callback1;
        OperationFinished += callback2;
        OperationFinished += callback3;
    

    private void Form1_Load(object sender, EventArgs e)
    
        SecondaryThread = new Thread(new ThreadStart(SecondaryThreadMethod));
        SecondaryThread.Start();
    

     private void SecondaryThreadMethod()
     
        Stopwatch sw = new Stopwatch();
        sw.Restart();

        OnOperationFinished(new MessageEventArg("test1"));
        OnOperationFinished(new MessageEventArg("test2"));
        OnOperationFinished(new MessageEventArg("test3"));
        //This is where the program waits for whatever operations take
             //place when OperationFinished is triggered.

        sw.Stop();

        Invoke((MethodInvoker)delegate
        
            richTextBox1.Text += "Time taken (ms): " + sw.ElapsedMilliseconds + "\n";
        );
     

    void callback1(object sender, MessageEventArg e)
    
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        
            richTextBox1.Text += e.Message + "\n";
        );
    
    void callback2(object sender, MessageEventArg e)
    
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        
            richTextBox1.Text += e.Message + "\n";
        );
    

    void callback3(object sender, MessageEventArg e)
    
        Thread.Sleep(2000);
        Invoke((MethodInvoker)delegate
        
            richTextBox1.Text += e.Message + "\n";
        );
    

    public event EventHandler<MessageEventArg> OperationFinished;

    protected void OnOperationFinished(MessageEventArg e)
    
        //##### Method1 - Event raised on the same thread ##### 
        //EventHandler<MessageEventArg> handler = OperationFinished;

        //if (handler != null)
        //
        //    handler(this, e);
        //

        //##### Method2 - Event raised on (the same) separate thread for all listener #####
        //EventHandler<MessageEventArg> handler = OperationFinished;

        //if (handler != null)
        //
        //    Task.Factory.StartNew(() => handler(this, e));
        //

        //##### Method3 - Event raised on different threads for each listener #####
        if (OperationFinished != null)
        
            foreach (EventHandler<MessageEventArg> handler in OperationFinished.GetInvocationList())
            
                Task.Factory.FromAsync((asyncCallback, @object) => handler.BeginInvoke(this, e, asyncCallback, @object), handler.EndInvoke, null);
            
        
    


public class MessageEventArg : EventArgs

    public string Message  get; set; 

    public MessageEventArg(string message)
    
        this.Message = message;
    

【讨论】:

【参考方案2】:

使用Task Parallel Library 现在可以执行以下操作:

Task.Factory.FromAsync( ( asyncCallback, @object ) => this.OperationFinished.BeginInvoke( this, EventArgs.Empty, asyncCallback, @object ), this.OperationFinished.EndInvoke, null );

【讨论】:

效果很好,感谢提醒 TPL 的 FromAsync 方法! @FactorMytic 你知道我可以在哪里阅读更多关于为什么它在这种情况下不起作用的信息吗? @piedar 派对有点晚了,但是 BeginInvoke 在多播委托上调用时抛出:***.com/questions/4731061/…【参考方案3】:

查看BackgroundWorker 类。我认为它完全符合您的要求。

编辑: 我认为您要问的是如何在仅完成整个后台任务的一小部分时触发事件。 BackgroundWorker 提供了一个名为“ProgressChanged”的事件,允许您向主线程报告整个过程的某些部分已完成。然后,当所有异步工作完成时,它会引发“RunWorkerCompleted”事件。

【讨论】:

不确定 BackgroundWorker 在这种情况下如何提供帮助。当然,当您需要通知时,将工作推送到单独的线程中是一个不错的选择,但在这种情况下,将处理程序推送到单独的线程中只是一个简单的工作项...... 如果我正在编写客户端应用程序,我可以让更新 GUI 的方法在后台工作程序中运行,这将阻止对 OperationFinished() 的调用阻止,但因为我不是在编写客户端应用程序我不能那样做。你是说我对 OpeartionFinished() 的调用应该在后台工作人员中进行吗?【参考方案4】:

我更喜欢将我传递给子线程的方法定义为更新 UI 的委托。首先定义一个委托:

public delegate void ChildCallBackDelegate();

在子线程中定义一个委托成员:

public ChildCallbackDelegate ChildCallback get; set;

在调用类中定义更新 UI 的方法。您需要将它包装在目标控件的调度程序中,因为它是从单独的线程中调用的。注意 BeginInvoke。在这种情况下,不需要 EndInvoke:

private void ChildThreadUpdater()

  yourControl.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background
    , new System.Threading.ThreadStart(delegate
      
        // update your control here
      
    ));

在启动子线程之前,设置它的 ChildCallBack 属性:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater);

那么当子线程要更新父线程时:

ChildCallBack();

【讨论】:

你能引用来源来支持不需要EndInvoke() 吗?我的理解是,确保调用线程资源始终是一种很好的做法,因为在特定情况下没有调用就不一定会释放线程资源。此外,您是否有理由选择使用 ThreadStart 而不是(相对)高性能的 ThreadPool?最后;此解决方案处理更新 UI,但我不认为 OP 的问题仅限于此 - 它不能解决异步引发事件的更广泛问题。 Jon Skeet 说得最好:***.com/questions/229554/…:“请注意,Windows 窗体团队已保证您可以以“即发即弃”的方式使用 Control.BeginInvoke - 即无需调用 EndInvoke。这通常异步调用并非如此:通常每个 BeginXXX 都应该有一个对应的 EndXXX 调用,通常在回调中。”另请注意,至少在 WPF 中,没有 Dispatcher.EndInvoke 方法。 我让我的解决方案更新了 UI,因为这是 OP 指定的:“当触发 OperationFinished 事件时,我希望客户端应用程序能够做任何它需要做的事情(即相应地更新 GUI)无需拖拽 API 操作。” ThreadPool 如果你没有太多线程就可以了,你想避免产生单独线程的开销,线程寿命相对较短并且线程是 CPU 密集型的。我最近使用线程的所有工作都涉及大量同时的网络连接,其中 ThreadStart 开销是无关紧要的,我希望有很多线程。我也从来不喜欢完整线程池的想法。 @ebpower:啊! Control.BeginInvoke() 与 Delegate.BeginInvoke() 完全不同,这是我搞混的地方。您的解决方案对于仅更新 UI 控件是可靠的,但它仍然不会将事件异步分派给所有侦听器 - 相反,它只是确保 UI 在正确的线程上更新。【参考方案5】:

所以您想以一种防止侦听器阻塞后台线程的方式引发事件?给我几分钟的时间来举个例子;这很简单:-)

我们开始:首先要注意! 每当您调用BeginInvoke 时,您必须调用相应的EndInvoke,否则如果调用的方法抛出异常返回一个值,那么 ThreadPool 线程将永远不会被释放回池中,从而导致线程泄漏!

class TestHarness


    static void Main(string[] args)
    
        var raiser = new SomeClass();

        // Emulate some event listeners
        raiser.SomeEvent += (sender, e) =>  Console.WriteLine("   Received event"); ;
        raiser.SomeEvent += (sender, e) =>
        
            // Bad listener!
            Console.WriteLine("   Blocking event");
            System.Threading.Thread.Sleep(5000);
            Console.WriteLine("   Finished blocking event");
        ;

        // Listener who throws an exception
        raiser.SomeEvent += (sender, e) =>
        
            Console.WriteLine("   Received event, time to die!");
            throw new Exception();
        ;

        // Raise the event, see the effects
        raiser.DoSomething();

        Console.ReadLine();
    


class SomeClass

    public event EventHandler SomeEvent;

    public void DoSomething()
    
        OnSomeEvent();
    

    private void OnSomeEvent()
    
        if (SomeEvent != null)
        
            var eventListeners = SomeEvent.GetInvocationList();

            Console.WriteLine("Raising Event");
            for (int index = 0; index < eventListeners.Count(); index++)
            
                var methodToInvoke = (EventHandler)eventListeners[index];
                methodToInvoke.BeginInvoke(this, EventArgs.Empty, EndAsyncEvent, null);
            
            Console.WriteLine("Done Raising Event");
        
    

    private void EndAsyncEvent(IAsyncResult iar)
    
        var ar = (System.Runtime.Remoting.Messaging.AsyncResult)iar;
        var invokedMethod = (EventHandler)ar.AsyncDelegate;

        try
        
            invokedMethod.EndInvoke(iar);
        
        catch
        
            // Handle any exceptions that were thrown by the invoked method
            Console.WriteLine("An event listener went kaboom!");
        
    

【讨论】:

为什么不直接调用多播委托,而不是使用 GetInvocationList? 您如何仅使用它异步调用事件侦听器?当然,您可以在单独的单个线程上调用 all 侦听器——我的解决方案确实将其提升到在自己的线程上调用 each 侦听器的水平——所以我可以看到这是矫枉过正。 按照我最初编写的方式,如果在客户端应用程序(没有侦听器)中没有处理事件的方法,客户端应用程序将抛出异​​常。您是否通过使用循环通过 eventListeners 的 for 循环来防止这种情况发生? 好的,我试过这种方法,效果很好!感谢您的帮助! @Jordan:很抱歉没有回答你问题的第二部分。上面的示例适用于所有void 代表,因为Delegate.EndInvoke() 不会返回值。对于具有返回类型的委托,每个返回类型需要有 1 个 EndAsyncEvent() 方法。【参考方案6】:

在事件委托上尝试 BeginInvoke 和 EndInvoke 方法 - 这些方法会立即返回,并允许您使用轮询、等待句柄或回调函数在方法完成时通知您。有关概述,请参阅here;在您的示例中,事件是您将使用的委托

【讨论】:

我不确定这是一个命名问题(你的意思是“事件委托”),但不要在事件字段上使用 BeginInvoke。您不能在多播委托上调用 BeginInvoke。即:BeginInvoke 不是异步 Invoke 子。【参考方案7】:

另外,如果我不想将任何参数传递给事件处理程序,使用 OperationFinished(null, new EventArgs()) 的语法是否正确?

没有。通常,您会这样称呼它:

OperationFinished(this, EventArgs.Empty);

您应该始终将对象作为发送者传递 - 它在模式中是预期的(尽管通常被忽略)。 EventArgs.Empty 也比 new EventArgs() 好。

为了在单独的线程中触发它,最简单的选择可能是只使用线程池:

private void RaiseOperationFinished()

       ThreadPool.QueueUserWorkItem( new WaitCallback( (s) =>
           
              if (this.OperationFinished != null)
                   this.OperationFinished(this, EventArgs.Empty);
           ));

话虽如此,在单独的线程上引发事件是应该彻底记录的,因为它可能会导致意外行为。

【讨论】:

@beruic 同意。这是 2009 年写的 ;) 我知道这是一个旧答案,但对使用 Task.Run 而不是 QueueUserWorkItem 的好处感到好奇?此外,如果想从中获得尽可能多的性能,UnsafeQueueUserWorkItem 会更快,如果我理解正确,我们唯一失去的就是 CAS(代码访问安全)(请参阅 Hans Passant @987654321 的精彩回答@关于UnsafeQueueUserWorkItem),这进一步缩短了事件被引发和事件处理程序实际运行之间的时间

以上是关于如何使事件处理程序异步运行?的主要内容,如果未能解决你的问题,请参考以下文章

.NET 中的异步 WMI 事件处理

使用异步回调处理事件

WCF 异步调用 - 事件处理程序中的异常

异步处理所有事件处理程序的方法?

javascript中事件处理程序的异步或同步调用

异步事件处理程序有时会挂在 Xamarin.Forms