È un tipo di cosa ibrido? (ad esempio, il mio programma .NET utilizza uno stack fino a quando non raggiunge una chiamata asincrona, quindi passa a un'altra struttura fino al completamento, a quel punto lo stack viene riavvolto in uno stato in cui può essere sicuro degli elementi successivi, ecc.? )
Fondamentalmente sì.
Supponiamo di avere
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
Ecco una spiegazione estremamente semplificata di come vengono reificate le continuazioni. Il vero codice è considerevolmente più complesso, ma questo rende l'idea.
Fai clic sul pulsante. Un messaggio è in coda. Il ciclo di messaggi elabora il messaggio e chiama il gestore di clic, mettendo nello stack l'indirizzo di ritorno della coda dei messaggi. Cioè, la cosa che succede dopo che il gestore è finito è che il ciclo di messaggi deve continuare a funzionare. Quindi la continuazione del gestore è il ciclo.
Il gestore di clic chiama Foo (), mettendo l'indirizzo di ritorno di se stesso nello stack. Cioè, la continuazione di Foo è il resto del gestore di clic.
Foo chiama Task.Delay, mettendo l'indirizzo di ritorno di se stesso nello stack.
Task.Delay fa tutto il necessario per restituire immediatamente un'attività. Lo stack è saltato fuori e siamo di nuovo a Foo.
Foo controlla l'attività restituita per vedere se è stata completata. Non è. La prosecuzione della attendono è quello di chiamare Blah (), in modo da Foo crea un delegato che chiama Blah (), e segni che delegare come la continuazione del compito. (Ho appena fatto una leggera dichiarazione errata; l'hai colta? In caso contrario, lo riveleremo tra un momento.)
Foo quindi crea il proprio oggetto Task, lo contrassegna come incompleto e lo restituisce nello stack al gestore dei clic.
Il gestore di clic esamina l'attività di Foo e scopre che è incompleta. La continuazione dell'attesa nel gestore è di chiamare Bar (), quindi il gestore di clic crea un delegato che chiama Bar () e lo imposta come continuazione dell'attività restituita da Foo (). Quindi restituisce lo stack al loop dei messaggi.
Il ciclo di messaggi continua a elaborare i messaggi. Alla fine la magia del timer creata dall'attività di ritardo fa la sua cosa e pubblica un messaggio in coda dicendo che ora è possibile eseguire la continuazione dell'attività di ritardo. Quindi il loop di messaggi chiama la continuazione dell'attività, mettendosi in pila come al solito. Quel delegato chiama Blah (). Blah () fa quello che fa e ritorna nello stack.
Ora che succede? Ecco la parte difficile. La continuazione dell'attività di ritardo non chiama solo Blah (). Deve anche attivare una chiamata a Bar () , ma quell'attività non conosce Bar!
Foo ha effettivamente creato un delegato che (1) chiama Blah () e (2) chiama la continuazione dell'attività che Foo ha creato e restituito al gestore dell'evento. Ecco come chiamiamo un delegato che chiama Bar ().
E ora abbiamo fatto tutto ciò che dovevamo fare, nell'ordine corretto. Ma non abbiamo mai smesso di elaborare i messaggi nel ciclo dei messaggi per molto tempo, quindi l'applicazione è rimasta reattiva.
Che questi scenari siano troppo avanzati per uno stack ha perfettamente senso, ma cosa sostituisce lo stack?
Un grafico di oggetti attività contenente riferimenti reciproci tramite le classi di chiusura dei delegati. Queste classi di chiusura sono macchine a stati che tengono traccia della posizione dell'attesa eseguita più di recente e dei valori dei locali. Inoltre, nell'esempio fornito, una coda di azioni a stato globale implementata dal sistema operativo e il ciclo di messaggi che esegue tali azioni.
Esercizio: come pensi che tutto funzioni in un mondo senza loop di messaggi? Ad esempio, applicazioni console. aspettare in un'app console è abbastanza diverso; puoi dedurre come funziona da quello che sai finora?
Quando ne avevo appreso anni fa, lo stack era lì perché era velocissimo e leggero, un pezzo di memoria allocato all'applicazione lontano dall'heap perché supportava una gestione altamente efficiente per l'attività a portata di mano (gioco di parole?). Che cosa è cambiato?
Le pile sono una struttura di dati utile quando le vite delle attivazioni del metodo formano uno stack, ma nel mio esempio le attivazioni del gestore di clic, Foo, Bar e Blah non formano uno stack. E quindi la struttura dei dati che rappresenta quel flusso di lavoro non può essere uno stack; piuttosto è un grafico delle attività e dei delegati allocati in heap che rappresenta un flusso di lavoro. Gli aspetti attesi sono i punti del flusso di lavoro in cui non è possibile compiere ulteriori progressi nel flusso di lavoro fino al completamento dei lavori avviati in precedenza; mentre stiamo aspettando, possiamo eseguire altri lavori che non dipendono dal completamento di quelle particolari attività avviate.
Lo stack è solo una matrice di frame, in cui i frame contengono (1) puntatori al centro delle funzioni (dove è avvenuta la chiamata) e (2) i valori delle variabili e delle temp locali. Le continuazioni delle attività sono la stessa cosa: il delegato è un puntatore alla funzione e ha uno stato che fa riferimento a un punto specifico nel mezzo della funzione (dove è avvenuta l'attesa) e la chiusura ha campi per ogni variabile locale o temporanea . I frame non formano più una bella matrice ordinata, ma tutte le informazioni sono uguali.