¿Cómo puedo hacer que un eventhandler se ejecute de forma asíncrona?


Estoy escribiendo un programa Visual de C# que ejecuta un bucle continuo de operaciones en un hilo secundario. Ocasionalmente, cuando ese hilo termina una tarea, quiero que active un eventhandler. Mi programa hace eso, pero cuando se activa el controlador de eventos, el subproceso secundario espera hasta que el controlador de eventos finalice antes de continuar el subproceso. ¿Cómo hago que continúe? Aquí está la forma en que actualmente lo tengo estructurado...

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.
  }

}

Este código es parte de una API para uno de mis dispositivo. Cuando se activa el evento OperationFinished, quiero que la aplicación cliente pueda hacer lo que necesite (es decir, actualizar la GUI en consecuencia) sin arrastrar la operación de la API.

Además, si no quiero pasar ningún parámetro al controlador de eventos, ¿mi sintaxis es correcta usando OperationFinished(null, new EventArgs()) ?

Author: PICyourBrain, 2009-12-16

7 answers

Entonces, ¿desea elevar el evento de una manera que evite que los oyentes bloqueen el hilo en segundo plano? Dame un par de minutos para preparar un ejemplo; es bastante simple: -)

Aquí vamos: primero una nota importante! Siempre que llame a BeginInvoke debe llamar al EndInvoke correspondiente, de lo contrario, si el método invocado arrojó una excepción o devolvió un valor, entonces el subproceso ThreadPool nunca se liberará de nuevo al repositorio, lo que resulta en una fuga de subprocesos!

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!");
        }
    }
}
 49
Author: STW,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-16 17:45:24

Además, si no quiero pasar ningún parámetro al controlador de eventos, ¿mi sintaxis es correcta usando OperationFinished(null, new EventArgs()) ?

No. Por lo general, lo llamarías como:

OperationFinished(this, EventArgs.Empty);

Siempre debe pasar un objeto como remitente - se espera en el patrón (aunque normalmente se ignora). EventArgs.Empty también es mejor que new EventArgs ().

Para disparar esto en un hilo separado, la opción más fácil es probablemente usar el grupo de temas:

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

Dicho esto, elevar un evento en un hilo separado es algo que debe documentarse a fondo, ya que potencialmente causará un comportamiento inesperado.

 11
Author: Reed Copsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-16 17:13:09

Con la Biblioteca Paralela de Tareas ahora es posible hacer lo siguiente:

Task.Factory.FromAsync( ( asyncCallback, @object ) => this.OperationFinished.BeginInvoke( this, EventArgs.Empty, asyncCallback, @object ), this.OperationFinished.EndInvoke, null );
 11
Author: WhiteKnight,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-02 11:00:29

Pruebe los métodos BeginInvoke y EndInvoke en el delegado de evento - estos regresan inmediatamente, y le permiten usar polling, un controlador de espera o una función de devolución de llamada para notificarle cuando el método se ha completado. Vea aquí para una descripción general; en su ejemplo, el evento es el delegado que usará

 6
Author: thecoop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-16 17:17:52

Tal vez Method2 o Method3 a continuación puede ayudar:)

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;
    }
}

}

 3
Author: Jiefeng Koh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-24 04:50:05

Prefiero definir un método que paso al subproceso hijo como un delegado que actualiza la interfaz de usuario. Primero defina un delegado:

public delegate void ChildCallBackDelegate();

En el subproceso hijo defina un miembro delegado:

public ChildCallbackDelegate ChildCallback {get; set;}

En la clase que llama defina el método que actualiza la interfaz de usuario. Tendrás que envolverlo en el despachador del control de destino ya que se llama desde un subproceso separado. Nota el BeginInvoke. En este contexto no se requiere EndInvoke:

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

Antes de iniciar el subproceso hijo, configure su propiedad ChildCallBack:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater);

Entonces cuando el subproceso hijo quiere actualizar el padre:

ChildCallBack();
 0
Author: Ed Power,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-16 23:29:51

Mira la clase BackgroundWorker. Creo que hace exactamente lo que usted está pidiendo.

EDITAR: Lo que creo que estás preguntando es cómo disparar un evento cuando solo una pequeña parte de la tarea de fondo general está completa. BackgroundWorker proporciona un evento llamado "ProgressChanged" que le permite informar al hilo principal que alguna parte del proceso general está completa. Luego, cuando todo el trabajo asincrónico se completa, se genera el evento" RunWorkerCompleted".

 0
Author: Mark Ewer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-17 00:25:03