Cosa succede esattamente quando un thread attende un'attività all'interno di un ciclo while?


10

Dopo aver avuto a che fare con il modello asincrono / attesa di C # da un po 'di tempo, improvvisamente ho capito che non so davvero come spiegare cosa succede nel seguente codice:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()si presume che restituisca un aspetto Taskche può o meno causare un cambio di thread quando viene eseguita la continuazione.

Non sarei confuso se l'attesa non fosse all'interno di un ciclo. Mi aspetto naturalmente che il resto del metodo (ovvero la continuazione) venga potenzialmente eseguito su un altro thread, il che va bene.

Tuttavia, all'interno di un ciclo, il concetto di "il resto del metodo" diventa un po 'confuso per me.

Cosa succede al "resto del loop" se il thread è attivato in continuazione vs. se non lo è? Su quale thread viene eseguita la successiva iterazione del loop?

Le mie osservazioni mostrano (non definitivamente verificato) che ogni iterazione inizia sullo stesso thread (quello originale) mentre la continuazione viene eseguita su un altro. Questo può essere davvero? In caso affermativo, si tratta di un grado di parallelismo inaspettato che deve essere tenuto in considerazione per la sicurezza dei thread del metodo GetWorkAsync?

AGGIORNAMENTO: La mia domanda non è un duplicato, come suggerito da alcuni. Il while (!_quit) { ... }modello di codice è semplicemente una semplificazione del mio codice attuale. In realtà, il mio thread è un ciclo di lunga durata che elabora la sua coda di input degli elementi di lavoro a intervalli regolari (ogni 5 secondi per impostazione predefinita). Il controllo effettivo della condizione di chiusura non è inoltre un semplice controllo sul campo come suggerito dal codice di esempio, ma piuttosto un controllo dell'handle dell'evento.



1
Vedi anche Come produrre e attendere implementare il flusso di controllo in .NET? per alcune grandi informazioni su come tutto questo è cablato insieme.
John Wu,

@ John Wu: non ho ancora visto quel thread SO. Molte pepite di informazioni interessanti lì. Grazie!
concluse il

Risposte:


6

Puoi effettivamente dare un'occhiata a Try Roslyn . Il metodo waitit viene riscritto nella void IAsyncStateMachine.MoveNext()classe asincrona generata.

Quello che vedrai è qualcosa del genere:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

Fondamentalmente, non importa su quale thread ti trovi; la macchina a stati può riprendere correttamente sostituendo il loop con una struttura if / goto equivalente.

Detto questo, i metodi asincroni non si eseguono necessariamente su un thread diverso. Vedi la spiegazione di Eric Lippert "Non è magico" per spiegare come puoi lavorare async/awaitsu un solo thread.


2
Mi sembra di sottovalutare l'estensione della riscrittura che il compilatore fa sul mio codice asincrono. In sostanza, non c'è "loop" dopo la riscrittura! Questa è stata la parte mancante per me. Fantastico e grazie anche per il link "Prova Roslyn"!
concluse il

GOTO è il costrutto loop originale . Non dimentichiamolo.

2

In primo luogo, Servy ha scritto del codice in una risposta a una domanda simile, su cui si basa questa risposta:

/programming/22049339/how-to-create-a-cancellable-task-loop

La risposta di Servy include un ContinueWith()ciclo simile che usa costrutti TPL senza l'uso esplicito delle parole chiave asynce await; quindi per rispondere alla tua domanda, considera quale potrebbe essere il tuo codice quando il tuo ciclo viene srotolato usandoContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Questo richiede del tempo per avvolgere la testa, ma in sintesi:

  • continuationrappresenta una chiusura per l ' "iterazione corrente"
  • previousrappresenta il Taskcontenimento dello stato della "precedente iterazione" (ovvero sa quando l '"iterazione" è terminata e viene utilizzata per iniziare la successiva ..)
  • Supponendo che GetWorkAsync()ritorni a Task, ciò significa ContinueWith(_ => GetWorkAsync())che restituirà a Task<Task>quindi la chiamata a Unwrap()per ottenere il 'compito interiore' (cioè il risultato effettivo di GetWorkAsync()).

Così:

  1. Inizialmente non esiste una precedente iterazione, quindi viene semplicemente assegnato un valore di Task.FromResult(_quit) - il suo stato inizia come Task.Completed == true.
  2. Il continuationviene eseguito per la prima volta conprevious.ContinueWith(continuation)
  3. gli continuationaggiornamenti di chiusura previousper riflettere lo stato di completamento di_ => GetWorkAsync()
  4. Al _ => GetWorkAsync()termine, "continua con" _previous.ContinueWith(continuation), ovvero chiamando continuationnuovamente lambda
    • Ovviamente a questo punto, previousè stato aggiornato con lo stato di _ => GetWorkAsync()così il continuationlambda viene chiamato quando GetWorkAsync()ritorna.

Il continuationlambda controlla sempre lo stato di _quitcosì, se _quit == falsepoi non ci sono più continuazioni, e TaskCompletionSourceviene impostato sul valore di _quit, e tutto è completato.

Per quanto riguarda la tua osservazione per quanto riguarda la continuazione eseguita in un thread diverso, questo non è qualcosa che la parola chiave async/ awaitfarebbe per te, come in questo blog "Le attività non sono (ancora) thread e asincrono non è parallelo" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

Suggerirei che vale davvero la pena dare un'occhiata più da vicino al tuo GetWorkAsync()metodo per quanto riguarda la filettatura e la sicurezza della filettatura. Se la tua diagnostica sta rivelando che è stata eseguita su un thread diverso come conseguenza del tuo codice asincrono / attesa ripetuto, qualcosa all'interno o correlato a quel metodo deve causare la creazione di un nuovo thread altrove. (Se questo è inaspettato, forse c'è un .ConfigureAwaitposto?)


2
Il codice che ho mostrato è (molto) semplificato. All'interno di GetWorkAsync () ci sono molte altre attese. Alcuni di essi accedono al database e alla rete, il che significa vero I / O. A quanto ho capito, l'interruttore del thread è una conseguenza naturale (sebbene non richiesta) di tali aspetti, poiché il thread iniziale non stabilisce alcun contesto di sincronizzazione che regolerebbe dove dovrebbero essere eseguite le continuazioni. Quindi vengono eseguiti su un thread del pool di thread. Il mio ragionamento è sbagliato?
avvenuta il

@aoven Un buon punto - non ho considerato i diversi tipi di SynchronizationContext- che è certamente importante poiché .ContinueWith()usa SynchronizationContext per inviare la continuazione; spiegherebbe davvero il comportamento che stai vedendo se awaitviene chiamato su un thread ThreadPool o su un thread ASP.NET. In questi casi, una continuazione potrebbe certamente essere inviata a un thread diverso. D'altra parte, invocare awaitun contesto a thread singolo come un dispatcher WPF o un contesto Winforms dovrebbe essere sufficiente per garantire che la continuazione avvenga sull'originale. thread
Ben Cottrell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.