Как заставить обработчик событий работать асинхронно?

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

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


person PICyourBrain    schedule 16.12.2009    source источник
comment
В каком потоке вы хотите вызвать событие OperationFinished? Это не может быть ваш вторичный поток, поскольку вы явно не требуете его блокировать. Должен ли он быть основным потоком, или вас устраивает, что он поднимается в другом потоке, созданном только для целей асинхронного обратного вызова?   -  person Pavel Minaev    schedule 16.12.2009


Ответы (7)


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

Итак: сначала важное замечание! Каждый раз, когда вы вызываете 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!");
        }
    }
}
person STW    schedule 16.12.2009
comment
Почему бы просто не вызвать делегата многоадресной рассылки напрямую, а не использовать GetInvocationList? - person thecoop; 17.12.2009
comment
Как бы вы могли асинхронно вызывать слушателей событий, просто используя это? Конечно, вы могли бы вызвать всех слушателей в отдельном единственном потоке - мое решение действительно доводит его до уровня вызова каждого слушателя в отдельном потоке - чтобы я мог видеть это перебор. - person STW; 17.12.2009
comment
Как я изначально писал, если в клиентском приложении не было метода обработки события (без слушателей), клиентское приложение генерировало исключение. Вы предотвращаете это, используя цикл for, который проходит через eventListeners? - person PICyourBrain; 18.12.2009
comment
Хорошо, я попробовал этот подход, и он отлично работает! Спасибо за помощь! - person PICyourBrain; 18.12.2009
comment
@ Yoooder: Не могли бы вы объяснить, что на самом деле делает EndAsyncEvent. Если у меня есть несколько событий, которые запускаются таким образом, могу ли я использовать один и тот же EndAsyncEvent для всех из них, или каждое событие нуждается в соответствующем методе EndAsyncEvent ???? - person PICyourBrain; 22.02.2010
comment
@Jordan: Прочтите, что я написал над образцом кода; EndAsyncEvent() существует полностью для обеспечения вызова EndInvoke(). Если не позвонить EndInvoke(), это может вызвать головную боль вроде ThreadPool - person STW; 24.02.2010
comment
@Jordan: извините за то, что не ответил на вторую часть вашего вопроса. Приведенный выше пример будет работать для всех void делегатов, поскольку Delegate.EndInvoke() не вернет значение. Для делегатов с возвращаемым типом потребуется 1 EndAsyncEvent() метод на каждый возвращаемый тип. - person STW; 24.02.2010
comment
OP упоминает об обновлении графического интерфейса; Для этого потребуется что-то вроде System.Windows.Application.Current.Dispatcher.BeginInvoke (methodToInvoke, null, EventArgs.Empty); для запуска функции в потоке графического интерфейса. Это также означает, что EndAsyncEvent () не используется, верно? - person Steve Hibbert; 18.04.2017
comment
как насчет ядра .net? есть ли альтернатива? System.PlatformNotSupportedException: Operation is not supported on this platform. - person Dwi Yanuar Ilham; 29.01.2020

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

Task.Factory.FromAsync( ( asyncCallback, @object ) => this.OperationFinished.BeginInvoke( this, EventArgs.Empty, asyncCallback, @object ), this.OperationFinished.EndInvoke, null );
person WhiteKnight    schedule 02.05.2013
comment
Отлично работает, спасибо, что напомнили FromAsync метод TPL! - person NumberFour; 04.01.2015
comment
@FactorMytic Знаете ли вы, где я мог бы узнать больше о том, почему это не работает в таком случае? - person piedar; 29.07.2016
comment
@piedar Немного поздно для вечеринки, но BeginInvoke выдает при вызове многоадресного делегата: stackoverflow.com/questions/4731061/ - person Søren Boisen; 20.03.2018

Кроме того, если я не хочу передавать какие-либо параметры обработчику событий, верен ли мой синтаксис с использованием 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);
           }));
}

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

person Reed Copsey    schedule 16.12.2009
comment
@beruic Согласен. Это было написано в 2009 году;) - person Reed Copsey; 30.04.2015
comment
Я знаю, что это старый ответ, но интересно узнать о пользе использования Task.Run вместо QueueUserWorkItem? Кроме того, если кто-то хочет выжать из него максимально возможную производительность, UnsafeQueueUserWorkItem быстрее, и единственное, что мы теряем, если я правильно понимаю, - это CAS (защита доступа к коду) (см. Отличный ответ Ханса Пассанта здесь относительно UnsafeQueueUserWorkItem), что дополнительно уменьшает время между возникновением события и временем фактического запуска обработчика событий. - person mhand; 03.01.2019

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

person thecoop    schedule 16.12.2009
comment
Я не уверен, что это проблема именования (что вы имеете в виду под делегатом события), но НЕ используйте BeginInvoke в поле события. Вы не можете вызвать BeginInvoke для делегатов многоадресной рассылки. Т.е. BeginInvoke - это не асинхронный подпрограмм Invoke. - person user1515791; 30.05.2018

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

}

person Jiefeng Koh    schedule 24.02.2015

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

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();
person Ed Power    schedule 16.12.2009
comment
Можете ли вы сослаться на источники, подтверждающие, что EndInvoke() не требуется? Насколько я понимаю, это всегда хорошая практика - гарантировать, что он вызывается, поскольку ресурсы потоковой передачи не обязательно освобождаются без вызова при определенных обстоятельствах. Кроме того, есть ли причина, по которой вы предпочитаете использовать ThreadStart, а не (относительно) производительный ThreadPool? Наконец; это решение обрабатывает обновление пользовательского интерфейса, но я не думаю, что вопрос OP ограничивался этим - оно не решает более широкую проблему асинхронного вызова событий. - person STW; 17.12.2009
comment
Джон Скит сказал это лучше всего: stackoverflow .com / questions / 229554 /: обратите внимание, что команда разработчиков Windows Forms гарантировала, что вы можете использовать Control.BeginInvoke в режиме «выстрелил и забыл», то есть без вызова EndInvoke. Это не относится к асинхронным вызовам в целом: обычно каждый BeginXXX должен иметь соответствующий вызов EndXXX, обычно в обратном вызове. Также обратите внимание, что, по крайней мере, в WPF нет метода Dispatcher.EndInvoke. - person Ed Power; 17.12.2009
comment
Я заставил свое решение обновлять пользовательский интерфейс, потому что это указано в OP: когда запускается событие OperationFinished, я хочу, чтобы клиентское приложение могло делать все, что ему нужно (т.е. обновлять графический интерфейс соответственно), не нарушая работу API. - person Ed Power; 17.12.2009
comment
ThreadPool подходит, если у вас не слишком много потоков, вы хотите избежать накладных расходов, связанных с созданием отдельного потока, время жизни потока относительно невелико и поток загружает процессор. Вся моя недавняя работа с потоками включает в себя множество одновременных сетевых подключений, где накладные расходы ThreadStart несущественны, и я хочу иметь много потоков. Мне также никогда не нравилась идея полного пула потоков. - person Ed Power; 17.12.2009
comment
@ebpower: Ааа! Control.BeginInvoke () - это совершенно другое животное, чем Delegate.BeginInvoke (), в котором я запутался. Тогда ваше решение является надежным для простого обновления элементов управления пользовательского интерфейса, но оно по-прежнему не отправляет событие всем слушателям асинхронно - вместо этого оно просто обеспечивает обновления пользовательского интерфейса в правильном потоке. - person STW; 20.12.2009
comment
Спасибо за комментарии. Но где ему понадобилось несколько слушателей? Он просто обновляет интерфейс. - person Ed Power; 21.12.2009

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

РЕДАКТИРОВАТЬ: Я думаю, вы спрашиваете, как запустить событие, когда завершена только небольшая часть общей фоновой задачи. BackgroundWorker предоставляет событие под названием «ProgressChanged», которое позволяет вам сообщить основному потоку, что некоторая часть всего процесса завершена. Затем, когда вся асинхронная работа завершена, он вызывает событие «RunWorkerCompleted».

person Mark Ewer    schedule 16.12.2009
comment
Не знаю, как BackgroundWorker помогает в этой ситуации. Конечно, это отличный вариант для передачи работы в отдельный поток, когда вам нужны уведомления, но в этом случае это просто простой рабочий элемент, чтобы отправить обработчик в отдельный поток ... - person Reed Copsey; 16.12.2009
comment
Если бы я писал клиентское приложение, у меня мог бы быть метод, который обновляет графический интерфейс, работающий в фоновом режиме, и который остановил бы блокировку вызова OperationFinished (), но, поскольку я не пишу клиентское приложение, я не могу этого сделать. Вы хотите сказать, что мой вызов OpeartionFinished () должен выполняться в фоновом работнике? - person PICyourBrain; 16.12.2009