web-dev-qa-db-fra.com

Comment faire fonctionner un gestionnaire d'événements de manière asynchrone?

J'écris un programme Visual C # qui exécute une boucle continue d'opérations sur un thread secondaire. Parfois, lorsque ce thread termine une tâche, je veux qu'il déclenche un gestionnaire d'événements. Mon programme fait cela, mais lorsque le gestionnaire d'événements est déclenché, le thread secondaire attend que le gestionnaire d'événements soit terminé avant de continuer le thread. Comment puis-je continuer? Voici comment je l'ai actuellement structuré ...

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

}

Ce code fait partie d'une API pour l'un de mes appareils. Lorsque l'événement OperationFinished est déclenché, je veux que l'application cliente puisse faire tout ce dont elle a besoin (c'est-à-dire mettre à jour l'interface graphique en conséquence) sans nuire à l'opération de l'API.

De plus, si je ne veux pas transmettre de paramètres au gestionnaire d'événements, ma syntaxe est-elle correcte en utilisant OperationFinished(null, new EventArgs())?

42
PICyourBrain

Vous souhaitez donc déclencher l'événement d'une manière qui empêche les auditeurs de bloquer le thread d'arrière-plan? Donnez-moi quelques minutes pour concocter un exemple; c'est assez simple :-)

C'est parti: d'abord une note importante! Chaque fois que vous appelez BeginInvoke, vous devez appeler le EndInvoke correspondant, sinon si la méthode invoquée a levé une exception ou a renvoyé une valeur, puis le thread ThreadPool ne sera jamais libéré dans le pool, ce qui entraînera une fuite de thread!

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

Avec la Task Parallel Library il est maintenant possible de faire ce qui suit:

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

De plus, si je ne veux transmettre aucun paramètre au gestionnaire d'événements, ma syntaxe est-elle correcte en utilisant OperationFinished (null, new EventArgs ())?

Non. En règle générale, vous l'appeleriez comme suit:

OperationFinished(this, EventArgs.Empty);

Vous devez toujours passer un objet en tant qu'expéditeur - il est attendu dans le modèle (bien que généralement ignoré). EventArgs.Empty est également meilleur que les nouveaux EventArgs ().

Afin de déclencher cela dans un thread séparé, l'option la plus simple est probablement d'utiliser simplement le pool de threads:

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

Cela étant dit, le fait de déclencher un événement sur un thread séparé est quelque chose qui doit être soigneusement documenté, car cela peut potentiellement provoquer un comportement inattendu.

12
Reed Copsey

Essayez les méthodes BeginInvoke et EndInvoke sur le délégué d'événement - elles retournent immédiatement et vous permettent d'utiliser l'interrogation, un handle d'attente ou une fonction de rappel pour vous avertir lorsque la méthode est terminée. Voir ici pour un aperçu; dans votre exemple, l'événement est le délégué que vous utiliserez

6
thecoop

Peut-être que la méthode 2 ou la méthode 3 ci-dessous peuvent vous aider :)

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

}

4
Jiefeng Koh

Je préfère définir une méthode que je passe au thread enfant en tant que délégué qui met à jour l'interface utilisateur. Définissez d'abord un délégué:

public delegate void ChildCallBackDelegate();

Dans le thread enfant, définissez un membre délégué:

public ChildCallbackDelegate ChildCallback {get; set;}

Dans la classe appelante, définissez la méthode qui met à jour l'interface utilisateur. Vous devrez l'envelopper dans le répartiteur du contrôle cible depuis son appel depuis un thread séparé. Notez BeginInvoke. Dans ce contexte, EndInvoke n'est pas requis:

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

Avant de lancer votre thread enfant, définissez sa propriété ChildCallBack:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater);

Ensuite, lorsque le thread enfant souhaite mettre à jour le parent:

ChildCallBack();
0
Ed Power

Regardez la classe BackgroundWorker . Je pense que cela fait exactement ce que vous demandez.

EDIT: Je pense que vous demandez comment déclencher un événement lorsque seule une petite partie de la tâche d'arrière-plan globale est terminée. BackgroundWorker fournit un événement appelé "ProgressChanged" qui vous permet de signaler au thread principal qu'une partie du processus global est terminée. Ensuite, lorsque tout le travail asynchrone est terminé, il déclenche l'événement "RunWorkerCompleted".

0
Mark Ewer