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

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”事件?它不能是你的辅助线程,因为你明确要求不要阻塞它。那么它必须是主线程吗?或者你是否可以接受它在新创建的线程上被触发,以便进行异步回调? - Pavel Minaev
7个回答

69

你想以一种防止监听器阻塞后台线程的方式触发事件?稍等几分钟,我会给你举个例子;这很简单 :-)

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

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!");
        }
    }
}

4
为什么不直接调用多播委托,而要使用GetInvocationList方法呢? - thecoop
2
你如何异步调用事件监听器?虽然你可以在单独的线程上调用所有监听器,但我的解决方案将每个监听器都调用到其自己的线程上,因此可能会过度。 - STW
好的,我尝试了这种方法,效果很棒!感谢您的帮助! - PICyourBrain
1
@Jordan:抱歉没有回答你问题的第二部分。上面的示例适用于所有 void 委托,因为 Delegate.EndInvoke() 不会返回值。对于具有返回类型的委托,每个返回类型都需要有 1 个 EndAsyncEvent() 方法。 - STW
OP提到需要更新GUI;这将需要类似于“System.Windows.Application.Current.Dispatcher.BeginInvoke(methodToInvoke, null, EventArgs.Empty);”这样的东西来在GUI线程上触发函数。这也意味着EndAsyncEvent()没有被使用,对吗? - Steve Hibbert
显示剩余4条评论

21

使用任务并行库(Task Parallel Library),现在可以做到以下事情:

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

非常好,感谢您提醒TPL的FromAsync方法! - NumberFour
1
@FactorMytic,你知道我在哪里可以阅读更多关于为什么在那种情况下它不起作用的信息吗? - piedar
2
@piedar 有点晚了,但是当在多路广播委托上调用BeginInvoke时会抛出异常:https://dev59.com/Wm445IYBdhLWcg3ws8dp - Søren Boisen

12

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

不是的。通常情况下,您应该这样调用:

OperationFinished(this, EventArgs.Empty);

在传递sender参数时,应该始终传递对象——这是该模式中的预期(尽管通常被忽略)。 EventArgs.Empty比new EventArgs()更好。

如果要在单独的线程中触发此事件,则最简单的选项可能是使用线程池:

private void RaiseOperationFinished()
{
       ThreadPool.QueueUserWorkItem( new WaitCallback( (s) =>
           {
              if (this.OperationFinished != null)
                   this.OperationFinished(this, EventArgs.Empty);
           }));
}

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


2
@beruic 同意。这是在2009年写的 ;) - Reed Copsey
我知道这是一个旧答案,但我很好奇使用Task.Run而不是QueueUserWorkItem的好处是什么?此外,如果想要尽可能地提高性能,UnsafeQueueUserWorkItem更快,我们唯一失去的东西(如果我理解正确)就是CAS(代码访问安全性)(请参见Hans Passant在此处关于UnsafeQueueUserWorkItem的绝佳答案),这进一步缩短了事件被触发和事件处理程序实际运行之间的时间。 - mhand

6
尝试使用BeginInvoke和EndInvoke方法来处理事件委托 - 这些方法会立即返回,并允许您使用轮询、等待句柄或回调函数来通知您方法何时完成。在此处查看概述:这里;在您的示例中,事件是您将要使用的委托。

我不确定这是否是一个命名问题(你所说的“事件委托”是什么意思),但是不要在事件字段上使用BeginInvoke。您无法在多播委托上调用BeginInvoke。即:BeginInvoke不是异步Invoke子程序。 - user1515791

4
也许下面的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;
    }
}

}


0

我更喜欢定义一个方法,将其作为委托传递给子线程,以更新用户界面。首先定义一个委托:

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 的问题不仅限于此 - 它没有解决异步触发事件的更广泛问题。 - STW
1
Jon Skeet说得最好:https://dev59.com/zXVC5IYBdhLWcg3woStW: “请注意,Windows Forms团队已经保证您可以以“点火并忘记”的方式使用Control.BeginInvoke - 即,从未调用EndInvoke。这在一般的异步调用中并不成立:通常每个BeginXXX都应该有一个相应的EndXXX调用,通常在回调中。”此外,请注意,至少在WPF中,没有Dispatcher.EndInvoke方法。 - Ed Power
我让我的解决方案更新了用户界面,因为这是 OP 指定的:“当 OperationFinished 事件被触发时,我希望客户端应用程序能够根据需要执行任何操作(即相应地更新 GUI),而不会停止 API 操作。” - Ed Power
如果你没有太多的线程,想要避免启动独立线程的开销,线程的生命周期相对较短且线程需要大量的CPU资源,那么线程池是一个不错的选择。我最近的工作涉及大量同时进行的网络连接,其中ThreadStart的开销微不足道,而且我希望有很多线程。此外,我从来不喜欢使用完整的线程池的概念。 - Ed Power
@ebpower:啊!Control.BeginInvoke()和Delegate.BeginInvoke()完全不同,这就是我混淆的地方。你的解决方案对于仅更新UI控件非常稳定,但它仍然不能异步地将事件分派给所有侦听器--相反,它只确保UI在正确的线程上更新。 - STW
谢谢留言。但是他在哪里要求多个侦听器?他只是更新用户界面。 - Ed Power

0

看一下 BackgroundWorker 类。我认为它正好满足你的需求。

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


2
不确定BackgroundWorker如何在这种情况下有所帮助。诚然,当您需要通知时将工作推入单独的线程是一个很好的选择,但在这种情况下,它只是一个简单的工作项,用于将处理程序推入单独的线程... - Reed Copsey
如果我正在编写客户端应用程序,我可以将更新GUI的方法运行在BackgroundWorker中,这将阻止对OperationFinished()的调用阻塞,但由于我没有编写客户端应用程序,我无法这样做。您是在说我的对OperationFinished()的调用应该在BackgroundWorker中吗? - PICyourBrain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接