Как заставить обработчик событий работать асинхронно? - PullRequest
37 голосов
/ 16 декабря 2009

Я пишу программу на 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())?

Ответы [ 7 ]

53 голосов
/ 16 декабря 2009

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

Здесь мы идем: первая важная заметка! Всякий раз, когда вы вызываете 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!");
        }
    }
}
12 голосов
/ 02 мая 2013

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

Task.Factory.FromAsync( ( asyncCallback, @object ) => this.OperationFinished.BeginInvoke( this, EventArgs.Empty, asyncCallback, @object ), this.OperationFinished.EndInvoke, null );
11 голосов
/ 16 декабря 2009

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

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

6 голосов
/ 16 декабря 2009

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

4 голосов
/ 24 февраля 2015

Может быть, метод 2 или метод 3 ниже могут помочь:)

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 голосов
/ 17 декабря 2009

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

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();
0 голосов
/ 16 декабря 2009

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

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

...