2009-11-15 15 views
17

Ho una certa familiarità con il multithreading in quanto ne ho letto ma non l'ho mai usato nella pratica.C# Multithreading - Richiama senza controllo

Ho un progetto che utilizza una libreria di terze parti che condivide lo stato di un dispositivo di input aumentando gli eventi. Il problema è che il modo in cui la libreria è scritta questi eventi viene sollevata da un thread diverso.

La mia applicazione non ha bisogno di essere multi-threaded e ho incontrato un sacco di problemi di threading classici (i controlli dell'interfaccia lamentano di essere interagiti da un thread diverso, le raccolte che vengono modificate man mano che un pezzo di codice sta iterando su di esso, ecc.).

Voglio solo che l'evento della libreria di terze parti venga restituito al mio thread dell'interfaccia utente. In particolare, ciò che penso dovrebbe accadere è:

La mia classe riceve l'evento e il gestore viene eseguito su un thread diverso rispetto all'interfaccia utente. Voglio rilevare questa condizione (come con InvokeRequired) e quindi eseguire l'equivalente di BeginInvoke per restituire il controllo al thread dell'interfaccia utente. Quindi le notifiche appropriate possono essere inviate sulla gerarchia di classi e tutti i miei dati vengono toccati solo da un thread.

Il problema è che la classe che riceve questi eventi di input non è derivata da Control e pertanto non dispone di InvokeRequired o BeginInvoke. Il motivo è che ho cercato di separare nettamente l'interfaccia utente e la logica sottostante. La classe è ancora in esecuzione sul thread dell'interfaccia utente, ma non ha alcuna interfaccia utente all'interno della classe stessa.

In questo momento ho risolto il problema rovinando questa separazione. Trasmetto un riferimento al controllo che visualizzerà i dati dalla mia classe e utilizzando i metodi i metodi Invoke. Sembra che sconfigga l'intero scopo di separarli perché ora la classe sottostante ha una dipendenza diretta dalla mia specifica classe UI.

Forse c'è un modo per salvare un riferimento al thread che ha eseguito il costruttore e quindi c'è qualcosa nello spazio dei nomi Threading che eseguirà i comandi Invoke?

C'è un modo per aggirare questo? Il mio approccio è completamente sbagliato?

risposta

18

Cerca nella classe AsyncOperation. Si crea un'istanza di AsyncOperation sul thread in cui si desidera chiamare il gestore utilizzando il metodo AsyncOperationManager.CreateOperation. L'argomento che uso per Create è solitamente nullo, ma è possibile impostarlo su qualsiasi cosa. Per chiamare un metodo su quel thread, utilizzare il metodo AsyncOperation.Post.

+0

Mi ci è voluto un po 'per capire come vengono utilizzati i delegati ma funziona! – colithium

1

Non è necessario il controllo specifico, qualsiasi controllo (incluso il modulo) lo farà. Quindi potresti in qualche modo astrarlo dall'interfaccia utente.

2

Il metodo di gestione potrebbe semplicemente archiviare i dati in una variabile membro della classe. L'unico problema con il cross-threading si verifica quando si desidera aggiornare i thread ai controlli non creati nel contesto di quel thread. Pertanto, la classe generica potrebbe ascoltare l'evento e quindi richiamare il controllo effettivo che si desidera aggiornare con una funzione di delega.

Anche in questo caso, è necessario richiamare solo i controlli dell'interfaccia utente che si desidera aggiornare per renderli sicuri. Qualche tempo fa ho scritto un post sul blog "Simple Solution to Illegal Cross-thread Calls in C#"

Il post va più nel dettaglio, ma il punto cruciale di un approccio molto semplice (ma limitato) è l'utilizzo di una funzione di delegato anonimo sul controllo dell'interfaccia utente che si desidera aggiornamento:

if (label1.InvokeRequired) { 
    label1.Invoke(
    new ThreadStart(delegate { 
     label1.Text = "some text changed from some thread"; 
    })); 
} else { 
    label1.Text = "some text changed from the form's thread"; 
} 

Spero che questo aiuti. L'opzione InvokeRequired è tecnicamente facoltativa, ma i controlli di richiamo sono piuttosto costosi, pertanto assicurarsi di non aggiornare label1.Text tramite il richiamo se non è necessario.

+0

Il mio problema è più lungo le linee di: thread di lavoro dalla libreria informa la mia app e voglio agire immediatamente. Il problema è che "l'azione immediata" verrà eseguita sul thread di lavoro, nascondendo completamente altre cose che accadono sul thread dell'interfaccia utente (cose che non sono correlate all'IU - enumerando liste di oggetti per esempio). – colithium

0

C'è un modo per aggirare questo?

Sì, la soluzione sarebbe creare una coda sicura per i thread.

  • vostro gestore di eventi viene richiamato dal thread 3rd-party
  • vostro gestore di eventi accoda qualcosa (dati di evento) su una raccolta (ad esempio una lista), che si possiede
  • il gestore di eventi non qualcosa da segnalare il tuo stesso, che ci sono i dati nella raccolta per farlo desistere ed elaborare:
    • La tua discussione potrebbe essere in attesa di qualcosa (un mutex o qualsiasi altra cosa); quando il suo mutex è segnalato dal gestore di eventi, si riattiva e controlla la coda.
    • In alternativa, invece di essere segnalato, potrebbe svegliarsi periodicamente (ad esempio una volta al secondo o qualsiasi altra cosa) e interrogare la coda.

In entrambi i casi, perché la coda è stato scritto da due fili diversi (il filo 3rd-party è enqueueing, e il thread è dequeueing), che deve essere una, la coda protetta thread-safe .

+0

Ho visto molti esempi in questo senso.Potrei sbagliarmi, ma questa tecnica non funzionerebbe se il dequeueing è il thread dell'interfaccia utente principale che non può andare a dormire e non può sedersi lì a interrogare la coda in un ciclo. Non ho menzionato questo ma l'altro thread solleva eventi molte volte al secondo. – colithium

+0

Hai ragione: questo sarebbe appropriato solo se il "tuo" thread che volevi richiamare non fosse * il thread dell'interfaccia utente. – ChrisW

13

Utilizzare SynchronizationContext.Current, che punterà a qualcosa che è possibile sincronizzare con.

Questo sarà fare la cosa giusta ™ a seconda del tipo di applicazione. Per un'applicazione WinForms, verrà eseguito sul thread dell'interfaccia utente principale.

In particolare, utilizzare il metodo SynchronizationContext.Send, in questo modo:

SynchronizationContext context = 
    SynchronizationContext.Current ?? new SynchronizationContext(); 

context.Send(s => 
    { 
     // your code here 
    }, null); 
+7

+1. Si noti che 'SynchronizationContext.Current' è nullo se il programma è un'applicazione console. Un buon pattern usa 'AsyncOperation' che cattura l'attuale' SynchronizationContext' sulla creazione e * fa la cosa giusta ++ * ™. http://msdn.microsoft.com/en-us/library/system.componentmodel.asyncoperation.aspx – dtb

+0

Non ero a conoscenza di quella lezione, grazie per avermelo fatto notare. –

+0

È WinForms, quindi avrebbe funzionato, ma la mia intenzione era di essere indipendente dall'interfaccia utente. L'app della console lo sta spingendo in termini di interfaccia utente che prenderei in considerazione, ma è utile sapere che non funzionerà. Ancora +1 – colithium

1

Se si utilizza WPF:

Hai bisogno di un riferimento all'oggetto Dispatcher che gestisce il thread dell'interfaccia utente. Quindi è possibile utilizzare il metodo Invoke o BeginInvoke sull'oggetto dispatcher per pianificare un'operazione che si svolge nel thread dell'interfaccia utente.

Il modo più semplice per ottenere il dispatcher sta utilizzando Application.Current.Dispatcher. Questo è il dispatcher responsabile del thread principale (e probabilmente l'unico) dell'interfaccia utente.

Mettere tutto insieme:

class MyClass 
{ 
    // Can be called on any thread 
    public ReceiveLibraryEvent(RoutedEventArgs e) 
    { 
     if (Application.Current.CheckAccess()) 
     { 
      this.ReceiveLibraryEventInternal(e); 
     } 
     else 
     { 
      Application.Current.Dispatcher.Invoke(
       new Action<RoutedEventArgs>(this.ReceiveLibraryEventInternal)); 
     } 
    } 

    // Must be called on the UI thread 
    private ReceiveLibraryEventInternal(RoutedEventArgs e) 
    { 
     // Handle event 
    } 
} 
+0

Si tratta di WPF, non vedo dove la domanda indica che viene utilizzato. –