Как сделать асинхронный запуск обработчика событий?

Я пишу программу Visual С#, которая выполняет непрерывный цикл операций над вторичным потоком. Иногда, когда этот поток заканчивает задачу, я хочу, чтобы он запускал обработчик событий. Моя программа делает это, но когда запускается обработчик событий, вторичный поток ожидает завершения обработчика события до продолжения потока. Как это сделать? Вот как я в настоящее время структурирован...

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, я хочу, чтобы клиентское приложение могло выполнять все, что ему нужно (например, обновлять графический интерфейс), не нарушая работу API.

Кроме того, если я не хочу передавать какие-либо параметры обработчику событий, мой синтаксис правильный, используя OperationFinished(null, new EventArgs())?

Ответ 1

Итак, вы хотите поднять событие так, чтобы слушатели не блокировали фоновый поток? Попробуйте пару минут, чтобы поднять пример; это довольно просто: -)

Здесь мы идем: сначала важное примечание!. Когда вы вызываете 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!");
        }
    }
}

Ответ 2

Кроме того, если я не хочу передавать какие-либо параметры обработчику событий, мой синтаксис корректен с помощью OperationFinished (null, new EventArgs())?

Нет. Обычно вы называете это:

OperationFinished(this, EventArgs.Empty);

Вы всегда должны передавать объект в качестве отправителя - он ожидается в шаблоне (хотя обычно игнорируется). EventArgs.Empty лучше, чем новый EventArgs(), а также.

Чтобы запустить это в отдельном потоке, проще всего использовать пул потоков:

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

Как говорится, создание события в отдельном потоке - это то, что должно быть тщательно документировано, так как это может вызвать неожиданное поведение.

Ответ 3

С помощью параллельной библиотеки задач теперь можно сделать следующее:

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

Ответ 4

Попробуйте методы BeginInvoke и EndInvoke в делетете событий - они немедленно возвращаются и позволяют использовать опрос, дескриптор ожидания или функцию обратного вызова, чтобы уведомить вас, когда метод завершился. См. здесь для обзора; в вашем примере событие является делегатом, который вы будете использовать

Ответ 5

Может быть, метод 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;
    }
}

}

Ответ 6

Посмотрите на класс BackgroundWorker. Я думаю, что это именно то, о чем вы просите.

EDIT: Я думаю, что вы спрашиваете, как запустить событие, когда завершена только небольшая часть общей фоновой задачи. BackgroundWorker предоставляет событие под названием "ProgressChanged", которое позволяет вам сообщать основному потоку, что часть этого процесса завершена. Затем, когда все асинхронные работы завершены, он вызывает событие "RunWorkerCompleted".

Ответ 7

Я предпочитаю определять метод, который я передаю дочернему потоку в качестве делегата, который обновляет пользовательский интерфейс. Сначала определите делегат:

public delegate void ChildCallBackDelegate();

В дочернем потоке определите член делегата:

public ChildCallbackDelegate ChildCallback {get; set;}

В вызывающем классе определите метод, который обновляет пользовательский интерфейс. Вам нужно будет обернуть его в диспетчере целевых диспетчеров, так как он вызывается из отдельного потока. Обратите внимание на 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();