Cosa succede esattamente quando un thread attende un'attività in un ciclo while?

8

Dopo aver a che fare con il modello di attesa / attesa di C # per un po 'di tempo, mi sono reso conto improvvisamente che non so davvero come spiegare ciò che accade nel seguente codice:

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

Si presume che GetWorkAsync() restituisca un% co_de attendibile che potrebbe o meno causare un cambio di thread quando viene eseguita la continuazione.

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

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

Che cosa succede a "il resto del ciclo" se il thread viene acceso in continuazione o se non viene cambiato? Su quale thread è la successiva iterazione del ciclo eseguito?

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

AGGIORNAMENTO: la mia domanda non è un duplicato, come suggerito da alcuni. Il modello di codice Task è semplicemente una semplificazione del mio codice attuale. In realtà, il mio thread è un ciclo di lunga durata che elabora la coda di input degli elementi di lavoro a intervalli regolari (ogni 5 secondi per impostazione predefinita). Anche il controllo della condizione di uscita effettiva non è un semplice controllo sul campo, come suggerito dal codice di esempio, ma piuttosto un controllo di gestione degli eventi.

    
posta aoven 19.02.2017 - 11:42
fonte

2 risposte

2

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

link

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

    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 un po 'di tempo per girare la testa, ma in sintesi:

  • continuation rappresenta una chiusura per "iterazione corrente"
  • previous rappresenta il Task che contiene lo stato di "iterazione precedente" (cioè sa quando è terminata l''iterazione' e viene utilizzata per avviare il successivo ..)
  • Supponendo che GetWorkAsync() restituisca un Task , ciò significa che ContinueWith(_ => GetWorkAsync()) restituirà un Task<Task> quindi la chiamata a Unwrap() per ottenere il 'compito interno' (cioè il risultato effettivo di GetWorkAsync() ).

Quindi:

  1. Inizialmente non esiste alcuna itazione precedente , quindi viene semplicemente assegnato un valore di Task.FromResult(_quit) - il suo stato inizia come Task.Completed == true .
  2. Il continuation viene eseguito per la prima volta utilizzando previous.ContinueWith(continuation)
  3. la continuation chiusure aggiorna previous per riflettere lo stato di completamento di _ => GetWorkAsync()
  4. Quando _ => GetWorkAsync() è completata, "continua con" _previous.ContinueWith(continuation) - cioè chiamando nuovamente continuation lambda
    • Ovviamente a questo punto, previous è stato aggiornato con lo stato di _ => GetWorkAsync() , quindi viene chiamato continuation lambda quando GetWorkAsync() restituisce.

Il continuation lambda controlla sempre lo stato di _quit quindi, se _quit == false non ci sono più continuazioni e TaskCompletionSource viene impostato sul valore di _quit , e tutto è completato.

Per quanto riguarda la tua osservazione sulla continuazione che viene eseguita in un thread diverso, non è qualcosa che la parola chiave async / await farebbe per te, come per questo blog "Le attività non sono (ancora) thread e async non sono paralleli ". - link

Suggerirei che vale la pena dare un'occhiata più da vicino al metodo GetWorkAsync() per quanto riguarda la sicurezza di threading e thread. Se la tua diagnostica rivela che è stata eseguita su un thread diverso come conseguenza del tuo codice asincrono / atteso ripetuto, qualcosa all'interno o in relazione a quel metodo deve causare la creazione di un nuovo thread altrove. (Se questo è inaspettato, forse c'è un .ConfigureAwait da qualche parte?)

    
risposta data 19.02.2017 - 12:36
fonte

Leggi altre domande sui tag