2014-05-02 14 views
7

In molti blog, tutorial e MSDN posso leggere che l'accesso agli elementi dell'interfaccia utente dal thread non-UI è impossibile - ok, otterrò un'eccezione non autorizzata. Per provarlo ho scritto un esempio molto semplice:Raising PropertyChanged in Task asincroni e thread UI

// simple text to which TextBlock.Text is bound 
private string sample = "Starting text"; 
public string Sample 
{ 
    get { return sample; } 
    set { sample = value; RaiseProperty("Sample"); } 
} 

private async void firstButton_Click(object sender, RoutedEventArgs e) 
{ 
    await Job(); // asynchronous heavy job 
    commands.Add("Element"); // back on UI thread so this should be ok? 
} 

private async Task Job() 
{ 
    // I'm not on UI Thread ? 
    await Task.Delay(2000); // some other job 
    Sample = "Changed"; // not ok as not UI thread? 
    commands.Add("Element from async"); // also not ok? 
} 

Ho un compito che è in esecuzione asincrona, in questo compito voglio: cambiare la mia proprietà (che consentirà di accrescere PropertyChanged) e aggiungere elementi alla ObservableCollection. Poiché viene eseguito async, non dovrei riuscire a farlo, ma non ottengo eccezioni e il codice funziona correttamente. Quindi i miei dubbi e incomprensioni:

  • perché non ottengo eccezione?
  • è ok per aumentare PropertyChanged in attività asincrona?
  • ok è possibile modificare ObservableCollecition in async Task, oppure devo restituire Task e dopo aver ottenuto il risultato modificare Observable - Clear it e Fill it?
  • quando sono in Attività sul thread dell'interfaccia utente e quando no?
  • nel codice sopra riportato in firstButton_Click è possibile gestire gli elementi dell'interfaccia utente dopo aver atteso l'attività? Sono sempre tornato sul thread dell'interfaccia utente?

Per provare più ho messo il mio cambiamento di proprietà e la modifica di raccolta in altro thread:

System.Threading.Timer newThreadTimer = new System.Threading.Timer((x) => 
    { 
     Sample = "Changed"; // not UI thread - exception 
     commands.Add("Element from async"); // not UI thread - exception 
    }, null, 1000, Timeout.Infinite); 

Nel codice di cui sopra il mio pensiero è ok - subito dopo la prima o la seconda linea ottengo un'eccezione . Ma cosa con il primo codice? È solo una fortuna che il mio compito sia stato eseguito sul thread dell'interfaccia utente?

Sospetto che questa sia una cosa fondamentale e il mio fraintendimento, ma ho bisogno di chiarimenti e quindi di questa domanda.

risposta

9

Quando attesa su un Task, la SynchronizationContext del thread corrente viene catturato (in particolare nel caso di Task dal TaskAwaiter). La continuazione viene quindi rimandata a quello SynchronizationContext per eseguire il resto del metodo (la parte dopo la parola chiave await).

Vediamo tuo esempio di codice:

private async Task Job() 
{ 
    // I'm not on UI Thread ? 
    await Task.Delay(2000); // some other job 
    Sample = "Changed"; // not ok as not UI thread? 
    commands.Add("Element from async"); // also not ok? 
} 

Quando si await Task.Delay(2000), il compilatore cattura implicitamente la SynchronizationContext, che è al momento il WindowsFormsSynchronizationContext. Quando l'attesa ritorna, la continuazione viene eseguita nello stesso contesto, dal momento che non hai detto esplicitamente di non farlo, che è il tuo thread dell'interfaccia utente.

Se è stato modificato il codice per await Task.Delay(200).ConfigureAwait(false), la continuazione non avrebbe siano radunate di nuovo al vostro corrente SynchronizationContext, e sarebbe eseguire un thread ThreadPool, causando l'aggiornamento elemento dell'interfaccia utente un'eccezione.

Nell'esempio del timer, l'evento Elapsed viene generato tramite un thread ThreadPool, quindi perché si ottiene un'eccezione che si sta tentando di aggiornare un elemento controllato da un thread diverso.

Ora, andiamo oltre la vostra domande una per una:

perché non posso ottenere eccezione?

Come detto, il await Task.Delay(2000) ha eseguito la Continuazione sul thread dell'interfaccia utente, che ha reso possibile aggiornare i controlli.

ok? Aumentare proprietà in async Attività?

io non sono sicuro di cosa si intende per "Alza le proprietà", ma se si intende sollevare un evento INotifyPropertyChanged, allora sì, è ok per eseguirli non in un contesto thread dell'interfaccia utente.

è ok per modificare ObservableCollecition in asincrono Task, o dovrei tornare attività e dopo aver ottenuto il risultato modificare Observable - Clear e riempirlo?

Se si dispone di un metodo async e si desidera aggiornare un elemento dell'interfaccia utente rilegato, assicuratevi di marshalling la continuazione sul thread dell'interfaccia utente. Se il metodo viene chiamato dal thread dell'interfaccia utente e il risultato è await, la continuazione verrà eseguita implicitamente sul thread dell'interfaccia utente. Nel caso in cui si desidera scaricare il lavoro ad un thread in background tramite Task.Run e assicurarsi che il proseguimento sia riceve l'interfaccia utente, è possibile catturare la vostra SynchronizationContext utilizzando TaskScheduler.FromCurrentSynchronizationContext() ed esplicitamente passare la continuazione

quando mi trovo in Task sulla UI filo e quando no?

A Task è una promessa di lavoro che verrà eseguita in futuro. quando si await su un TaskAwaitable dal contesto del thread dell'interfaccia utente, si è ancora in esecuzione sul thread dell'interfaccia utente. Non sei in thread dell'interfaccia utente se:

  1. Il tuo metodo asincrono è attualmente in esecuzione da un thread diverso, allora il thread UI (o un filo ThreadPool o un nuovo Thread)
  2. È Offload lavoro ad un thread in background ThreadPool utilizzando Task.Run.

nel codice sopra in firstButton_Click è ok per gestire elementi dell'interfaccia utente dopo in attesa del compito? Sono sempre tornato sul thread dell'interfaccia utente?

Sarete di nuovo al thread UI finché non dite esplicitamente il codice di non tornare al suo contesto corrente utilizzando ConfigureAwait(false)

+0

Vorrei fare +1 se non questa affermazione: * "La Libreria parallela attività utilizza un' TaskScheduler' per implicitamente (o esplicitamente ...) cattura l'attuale 'SynchronizationContext' ..." * Questo non è del tutto corretto per le continuazioni 'await': http://stackoverflow.com/q/23071609/1768303 – Noseratio

+0

Risposta lunga (grazie per questo), ma ho alcuni dubbi: 1. Guarda che il mio lavoro è eseguito come asincrono 'Attendi Job();' da Click - il contesto dell'interfaccia utente così catturato è prima e dopo non dentro? 2. Sono confuso dal ritorno dallo stesso thread - per esempio [Non posso aspettare dentro Mutex] (http://stackoverflow.com/q/23153155/2681948), quindi dopo aver restituito "il contesto catturato potrebbe significare qualsiasi thread thread-pool" 3. Se è ok per sollevare la proprietà modificata da async Task (su thread diversi), quindi perché non il Timer? – Romasz

+1

@Romasz si confondono i relativi concetti differenti di 'asincrono' /' sincrono' e 'concorrente'. Tutto il codice che hai scritto è SINGOLO FILETTATO, il che significa che non hai problemi. L'errore da principiante con 'async' /' await' è pensare, giusto ... re asincrono, significa thread giusto? I thread significano 'concurrent', e in passato era l'unico modo per ottenere' asincrono'. – Aron

2

L'evento PropertyChanged viene inviato automaticamente al thread UI per WPF quindi puoi modificare le proprietà che lo sollevano da qualsiasi thread. Prima di .NET 4.5, questo non era il caso dell'evento ObservableCollection.CollectionChanged e quindi, aggiungendo o rimuovendo elementi da un thread diverso da quello dell'interfaccia utente, si sarebbe verificata un'eccezione.

L'avvio di .NET 4.5 CollectionChanged viene inoltre inviato automaticamente al thread dell'interfaccia utente, quindi non è necessario preoccuparsi di ciò. Detto questo, dovrai comunque accedere agli elementi dell'interfaccia utente (ad esempio un pulsante, ad esempio) dal thread dell'interfaccia utente.

+1

. WPF viene sempre inviato al thread dell'interfaccia utente tramite binding. Il problema è che non ha mai supportato la modifica delle raccolte al di fuori del thread dell'interfaccia utente. Questo in genere si manifesta nella classe 'CollectionViewSource' che non supporta blah blah blah. – Aron