2015-08-25 7 views
8

Ho un'app WinForms e ho del codice che deve essere eseguito sul thread dell'interfaccia utente. Tuttavia, il codice dopo lo await viene eseguito su un thread diverso.attende senza ConfigureAwait (false) continua su un thread diverso

protected override async void OnHandleCreated(EventArgs e) 
{ 
    base.OnHandleCreated(e); 

    // This runs on the UI thread. 
    mainContainer.Controls.Clear(); 

    var result = await DoSomethingAsync(); 

    // This also needs to run on the UI thread, but it does not. 
    // Instead it throws an exception: 
    // "Cross-thread operation not valid: Control 'mainContainer' accessed from a thread other than the thread it was created on" 
    mainContainer.Controls.Add(new Control()); 
} 

Ho anche provato ad aggiungere in modo esplicito ConfigureAwait(true), ma non fa differenza. La mia comprensione è stata che se ometto lo ConfigureAwait(false), la continuazione dovrebbe essere eseguita sul thread originale. È sbagliato in alcune situazioni?

Ho anche notato che se aggiungo un controllo alla raccolta prima dell'attesa, la continuazione viene eseguita magicamente sul thread corretto.

protected override async void OnHandleCreated(EventArgs e) 
{ 
    base.OnHandleCreated(e); 

    // This runs on the UI thread. 
    mainContainer.Controls.Add(new Control()); 
    mainContainer.Controls.Clear(); 

    var result = await DoSomethingAsync(); 

    // This also runs on the UI thread now. Why? 
    mainContainer.Controls.Add(new Control()); 
} 

La mia domanda è:

  1. Perché succede questo?
  2. Come convinco la continuazione per l'esecuzione sul thread dell'interfaccia utente (idealmente senza fare il mio hack di aggiungere un controllo e rimuoverlo)?

Per riferimento, ecco le parti importanti di DoSomethingAsync. Invia una richiesta HTTP usando RestSharp.

protected async Task DoSomethingAsync() 
{ 
    IRestRequest request = CreateRestRequest(); 

    // Here I await the response from RestSharp. 
    // Client is an IRestClient instance. 
    // I have tried removing the ConfigureAwait(false) part, but it makes no difference. 
    var response = await Client.ExecuteTaskAsync(request).ConfigureAwait(false); 

    if (response.ResponseStatus == ResponseStatus.Error) 
     throw new Exception(response.ErrorMessage ?? "The request did not complete successfully."); 

    if (response.StatusCode >= HttpStatusCode.BadRequest) 
     throw new Exception("Server responded with an error: " + response.StatusCode); 

    // I also do some processing of the response here; omitted for brevity. 
    // There are no more awaits. 
} 
+1

Perché pensi che funzioni su un thread diverso? Dove viene eseguito questo codice? gestore di eventi? – i3arnon

+0

So che viene eseguito su un thread diverso perché il codice dopo l'attesa genera un'eccezione: "Operazione cross-thread non valida: controllo 'mainContainer' a cui si accede da un thread diverso dal thread su cui è stato creato". –

+0

Sì, il codice viene eseguito in un gestore eventi. Aggiornerò la mia domanda –

risposta

1

Sembra che qualcosa di strano sta accadendo con OnHandleCreated. La mia soluzione era usare OnLoad invece. Sono abbastanza contento di questa soluzione perché non c'è davvero alcun motivo per utilizzare OnHandleCreated nella mia situazione.

Sono ancora curioso di sapere perché questo sta accadendo, quindi se qualcuno lo sa, sentitevi liberi di inviare un'altra risposta.

Edit:

ho trovato il vero problema: si scopre che mi stava chiamando Form.ShowDialog() dopo un ConfigureAwait(false). In quanto tale, il modulo veniva costruito sul thread dell'interfaccia utente, ma in quel momento stavo chiamando ShowDialog su un thread non dell'interfaccia utente. Sono sorpreso che questo ha funzionato a tutti.

Ho rimosso il ConfigureAwait(false) così ora ShowDialog viene chiamato sul thread dell'interfaccia utente.

+0

Puoi posizionare 'Debug.WriteLine (new {SynchronizationContext.Current})' prima che 'attenda DoSomethingAsync() 'all'interno di' OnHandleCreated'? Mostra 'System.Windows.Forms.WindowsFormsSynchronizationContext' (previsto) o' System.Threading.SynchronizationContext'? – Noseratio

+1

Ricevo un 'System.Threading.SynchronizationContext' prima dell'attesa e nullo dopo l'attesa. Non sono sicuro del motivo per cui ieri ho ricevuto un valore non nullo. Ma immagino che lo spieghi. Ho anche notato che se eseguo lo stesso codice nello stesso progetto su un ramo diverso nel nostro repository, ottengo un 'System.Windows.Forms.WindowsFormsSynchronizationContext', e tutto funziona correttamente. Mi chiedo se abbia qualcosa a che fare con il modo in cui il modulo viene inizializzato. –

+0

Con la modifica recente, è necessario contrassegnare la risposta come accettata in quanto spiega cosa è effettivamente accaduto.Altrimenti è sicuro usare 'await' in' OnHandleCreated', vedere il mio [commento] (http://stackoverflow.com/questions/32211598/await-without-configureawaitfalse-continues-on-a-different-thread/32213206?noredirect = 1 # comment52321127_32213644) alla risposta di Stefano. – Noseratio

7

La mia comprensione era che se ometto ConfigureAwait (false), la continuazione dovrebbe essere eseguita sul thread originale. È sbagliato in alcune situazioni?

Cosa succede in realtà è che await catturerà l'attuale contesto di default, e utilizzare questo contesto, per riprendere il metodo async. Questo contesto è SynchronizationContext.Current, a meno che non sia null, nel qual caso è TaskScheduler.Current (in genere il contesto del pool di thread). Il più delle volte, il thread dell'interfaccia utente ha un'interfaccia utente SynchronizationContext - nel caso di WinForms, un'istanza di WinFormsSynchronizationContext.

Ho anche notato che se aggiungo un controllo alla raccolta prima dell'attesa, la continuazione viene eseguita magicamente sul thread corretto.

Nessun thread inizia automaticamente con un SynchronizationContext. WinForms SynchronizationContext viene installato su richiesta al momento della creazione del primo controllo. Questo è il motivo per cui lo vedi riprendere su un thread dell'interfaccia utente dopo aver creato un controllo.

Poiché il passaggio a OnLoad è una soluzione praticabile, vi consiglio di andare con quello. L'unica altra opzione (per riprendere il thread dell'interfaccia utente prima della creazione di un controllo) consiste nel creare manualmente un controllo prima del primo await.

+0

In realtà, 'WindowsFormsSynchronizationContext' viene installato nel costruttore di' Control' [qui] (http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/ System/WinForms/Control.cs, 535), quindi dovrebbe essere già presente quando viene chiamato 'OnHandleCreated'. Questo deve essere qualcos'altro. Esiste probabilmente un [bug] correlato (https://connect.microsoft.com/VisualStudio/feedback/details/806432/application-doevents-resets-the-threads-current-synchronization-context), ma in questo caso entrambi 'OnHandleCreated 'e' OnLoad' sarebbe interessato. – Noseratio