2009-12-16 19 views
31

Sto scrivendo un programma Visual C# che esegue un ciclo continuo di operazioni su un thread secondario. Occasionalmente quando quel thread termina un'attività, voglio che attivi un eventhandler. Il mio programma lo fa, ma quando viene attivato il gestore eventi, il thread secondario attende fino al termine del gestore eventi prima di continuare il thread. Come posso farlo continuare? Ecco il modo in cui attualmente lo ho strutturato ...Come faccio a far funzionare un eventhandler in modo asincrono?

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

} 

Questo codice fa parte di un'API per uno dei miei dispositivi. Quando viene attivato l'evento OperationFinished, voglio che l'applicazione client sia in grado di fare tutto ciò che è necessario (cioè aggiornare la GUI di conseguenza) senza interferire con l'operazione API.

Inoltre, se non si desidera passare alcun parametro al gestore eventi, la mia sintassi è corretta utilizzando OperationFinished(null, new EventArgs())?

+0

quale thread vuoi l'evento 'OperationFinished' di essere sollevato su? Non può essere il tuo thread secondario, dal momento che richiede esplicitamente di non bloccarlo. Deve essere il thread primario, quindi, o stai bene con il fatto di essere generato su un thread diverso appena creato solo allo scopo di callback asincrono? –

risposta

44

Quindi vuoi sollevare l'evento in modo da impedire agli ascoltatori di bloccare il thread in background? Dammi un paio di minuti per fare un esempio; è piuttosto semplice :-)

Eccoci: prima una nota importante! Ogni volta che si chiama BeginInvoke è necessario chiamare il corrispondente EndInvoke, altrimenti se il metodo richiamato ha generato un'eccezione o restituito un valore, il thread ThreadPool non verrà mai rilasciato nuovamente nel pool, causando una perdita di 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!"); 
     } 
    } 
} 
+2

Perché non chiamare semplicemente il delegato multicast direttamente anziché utilizzare GetInvocationList? – thecoop

+1

Come chiamereste gli ascoltatori di eventi in modo asincrono solo usando quello? Certo, puoi chiamare * tutti * gli ascoltatori su un singolo thread separato - la mia soluzione lo porta al livello di chiamata * ogni * listener sul proprio thread - quindi ho potuto vederlo essere eccessivo. – STW

+0

Il modo in cui avevo originariamente scritto il mio, se non ci fosse alcun metodo per gestire l'evento nell'app client (nessun listener) l'app client genererebbe un'eccezione. Eviti che ciò accada usando quel ciclo per loop che attraversa gli eventListeners? – PICyourBrain

0

Guarda la classe BackgroundWorker. Penso che faccia esattamente quello che stai chiedendo.

MODIFICA: Quello che penso si stia chiedendo è come attivare un evento quando è completata solo una piccola parte dell'attività complessiva in background. BackgroundWorker fornisce un evento chiamato "ProgressChanged" che consente di riportare al thread principale che una parte del processo generale è completa. Quindi, quando tutto il lavoro asincrono è completo, solleva l'evento "RunWorkerCompleted".

+1

Non sono sicuro di come BackgroundWorker aiuti in questa situazione. Certo, è una grande opzione per spingere il lavoro in un thread separato quando hai bisogno di notifiche, ma in questo caso, è solo un semplice oggetto di lavoro per spingere il gestore in un thread separato ... –

+0

Se stavo scrivendo l'applicazione client, io potrebbe avere il metodo che aggiorna la GUI eseguita in un backgroundworker e che fermerebbe la chiamata a OperationFinished() dal blocco, ma poiché non sto scrivendo l'app client non posso farlo. Stai dicendo che la mia chiamata a OpeartionFinished() dovrebbe essere all'interno di un backgroundworker? – PICyourBrain

11

Inoltre, se non si desidera passare alcun parametro al gestore eventi, la mia sintassi è corretta utilizzando OperationFinished (null, new EventArgs())?

No. In genere, si sarebbe chiamata come:

OperationFinished(this, EventArgs.Empty); 

Si dovrebbe sempre passare un oggetto come mittente - ci si aspetta nel modello (anche se in genere ignorato). EventArgs.Empty è anche meglio del nuovo EventArgs().

Al fine di sparare questa in un thread separato, l'opzione più semplice è probabilmente quella di utilizzare solo il pool di thread:

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

Detto questo, sollevando un evento su un thread separato è qualcosa che dovrebbe essere accuratamente documentato, in quanto potrebbe causare comportamenti imprevisti.

+2

Oggi, userei 'Task.Run' invece del pool di thread. – beruic

+1

@beruic Concordato. Questo è stato scritto nel 2009;) –

6

Provare i metodi BeginInvoke e EndInvoke sul delegato dell'evento, che vengono restituiti immediatamente e consentono di utilizzare il polling, un handle di attesa o una funzione di callback per notificare quando il metodo è stato completato. Vedi here per una panoramica; nel tuo esempio, l'evento è il delegato che utilizzerai

0

Preferisco definire un metodo che si passa al thread figlio come delegato che aggiorna l'interfaccia utente. Prima di definire un delegato:

public delegate void ChildCallBackDelegate(); 

Nel thread figlio definire un membro delegato:

public ChildCallbackDelegate ChildCallback {get; set;} 

Nella classe chiamata definire il metodo che aggiorna l'interfaccia utente. Avrai bisogno di avvolgerlo nel dispatcher del controllo target dal momento che viene chiamato da un thread separato. Nota il BeginInvoke. In questo contesto EndInvoke non è richiesto:

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

Prima di lanciare il tuo thread figlio, impostare la proprietà ChildCallBack:

theChild.ChildCallBack = new ChildCallbackDelegate(ChildThreadUpdater); 

Poi, quando il filo bambino vuole aggiornare il genitore:

ChildCallBack(); 
+0

Puoi citare fonti per eseguire il backup che 'EndInvoke()' non è richiesto? La mia comprensione è che è sempre buona pratica assicurarsi che venga chiamato poiché le risorse di threading non vengono necessariamente liberate senza la chiamata in circostanze specifiche. Inoltre, c'è un motivo per cui scegli di utilizzare un ThreadStart piuttosto che il ThreadPool (relativamente) performante? Da ultimo; questa soluzione gestisce l'aggiornamento dell'interfaccia utente, ma non credo che la domanda dell'OP fosse limitata a questo: non risolve il problema più generale di generare eventi in modo asincrono. – STW

+1

Jon Skeet lo ha detto meglio: http://stackoverflow.com/questions/229554/whats-the-difference-between-invoke-and-begininvoke: "Si noti che il team di Windows Form ha garantito che è possibile utilizzare Control.BeginInvoke in un modo "ignora e dimentica" - cioè senza mai chiamare EndInvoke. Questo non è vero per le chiamate asincrone in generale: normalmente ogni BeginXXX dovrebbe avere una chiamata EndXXX corrispondente, di solito nella callback. " Si noti inoltre che, almeno con WPF, non esiste alcun metodo Dispatcher.EndInvoke. –

+0

Ho reso la mia soluzione aggiornare l'interfaccia utente perché è quello che l'OP ha specificato: "Quando viene attivato l'evento OperationFinished voglio che l'applicazione client sia in grado di fare tutto ciò che è necessario (cioè aggiornare la GUI di conseguenza) senza ripetere l'operazione API." –

8

Con il Task Parallel Library è ora possibile effettuare le seguenti operazioni:

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

Funziona alla grande, grazie per aver ricordato il metodo 'FromAsync' di TPL! – NumberFour

+0

Intelligente, ma non funziona per i delegati multicast –

+1

@FactorMytic Sapete dove potrei leggere di più sul perché non funziona in quel caso? – piedar

3

Forse Method2 o method3 di seguito possono aiutare :)

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

}

Problemi correlati