CodeGym /Corsi /C++ SELF /std::future: get(), wait(), wait_for() e timeout

std::future: get(), wait(), wait_for() e timeout

C++ SELF
Livello 71, Lezione 1
Disponibile

1. Fondamenti di std::future: stato condiviso, wait() e get()

Quando un principiante vede std::future<int>, il primo pensiero spesso e' questo: «Ah, e' come int, solo pigro: adesso e' vuoto, poi si riempira'». La metafora e' quasi corretta, ma con un'importante precisazione: future non e' il valore stesso, ma un handle verso uno stato condiviso, dove prima o poi comparira' un valore (oppure qualcosa andra' storto).

E' comodo immaginarlo cosi': da qualche parte esiste una «scatolina del risultato» (nello standard la chiamano shared state), e std::future<T> e' il vostro biglietto, con cui potete o aspettare, oppure ritirarne il contenuto. E il biglietto, di regola, e' uno solo: un solo consumatore.

Questo «stato condiviso del risultato» non e' un'entita' mitica, ma una parte reale del modello future/promise. In particolare, e' importante ricordare che future::get() rilascia il legame con questo stato: il biglietto «si consuma» dopo l'uso.

wait() e get(): due azioni simili con significati diversi

Su questo tema molti inciampano, perché all'esterno tutto sembra uguale: «sia li' che qui stiamo aspettando». La differenza e' che:

  • wait() significa «aspetta finché non e' pronto»;
  • get() significa «aspetta e dammi il risultato».

Suona simile, ma le conseguenze sono diverse, e nel codice reale questo influisce sulla struttura del programma.

wait(): attendere la disponibilita', ma senza ritirare nulla

wait() serve quando per voi e' importante attendere il completamento, ma il valore in sé o non vi serve subito, oppure pensate di ritirarlo piu' tardi (magari in un altro punto del codice). Immaginate di aver messo una pizza nel forno: wait() e' «stare davanti al forno e aspettare il timer», ma senza ancora tirare fuori la pizza.

Mini-esempio: avviamo un task, facciamo qualcos'altro e poi ci limitiamo a verificare che il task sia terminato.

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::milliseconds{300});
        return 7;
    });

    f.wait();                 // atteso finché e' pronto
    std::cout << "ready\n";   // ready
}

Punto importante: dopo wait() il risultato e' ancora dentro la «scatolina», e in seguito potete chiamare get() e ritirarlo (se si tratta di future<T>, e non di future<void>).

get(): attendere e ritirare il risultato, «bruciare il biglietto»

get() fa due cose: aspetta finché il risultato non e' pronto e restituisce il valore (oppure nulla, se T = void). Allo stesso tempo, get() di solito rende il future «vuoto» — cioe', dopo un get() riuscito perde il collegamento con lo stato condiviso del risultato.

Il senso di questa natura monouso fa parte del contratto di future: non si puo' ritirare il risultato una seconda volta tramite lo stesso std::future.

Mini-esempio dimostrativo:

#include <future>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] { return 10; });

    int x = f.get();
    std::cout << x << '\n';   // 10

    // Un secondo get() e' un errore logico (il future e' gia' "consumato")
}

Qui non facciamo intenzionalmente un secondo get(), per non trasformare la lezione in un festival di eccezioni e terminazioni improvvise. Ma ricordate l'idea: get() e' un'azione con conseguenze.

2. Attesa con timeout: wait_for(), stati e wait_until()

Perché «aspettare per sempre» e' un cattivo modello di business

Negli esempi didattici spesso si scrive f.get() e si e' soddisfatti. Nei programmi reali questo a volte si trasforma in «siamo bloccati per sempre, ma almeno in modo rigorosamente conforme allo standard». Se state realizzando un'utilita' CLI, un server o anche semplicemente un'applicazione che deve restare viva, vi serve un modo per dire: «Aspettero' ancora un po', e se non e' pronto mi occupero' di qualcos'altro».

Per questo std::future ha una famiglia di attese con timeout: wait_for() e wait_until(). Il focus principale e' su wait_for().

wait_for(): «aspetta non piu' di N tempo» e restituisci uno stato

wait_for(duration) e' un modo di aspettare con un limite di tempo: specificate una durata (per esempio 100 ms), e il future restituisce uno stato che indica cosa e' successo in quell'intervallo.

L'idea chiave: il timeout non e' «annullare il task». E' soltanto un'osservazione: «in 100 ms il risultato non ha fatto in tempo a diventare pronto». Di regola, il task continua a essere eseguito.

Lo stato viene descritto tramite std::future_status. In pratica, i casi piu' frequenti sono ready e timeout, ma esiste anche deferred (avvio differito).

Tabella degli stati di std::future_status

Stato Cosa significa in parole semplici Cosa fare di solito
std::future_status::ready
il risultato e' pronto, si puo' chiamare get() ritirare il risultato
std::future_status::timeout
in questo intervallo non ha fatto in tempo continuare ad aspettare, mostrare il progresso, fare altro
std::future_status::deferred
il task e' differito e verra' eseguito al momento di wait()/get() capire che non c'e' parallelismo e decidere se va bene cosi'

Mini-esempio: verifica «e' pronto entro 100 ms?»

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::seconds{1});
        return 123;
    });

    auto st = f.wait_for(std::chrono::milliseconds{100});
    std::cout << (st == std::future_status::timeout) << '\n'; // 1
}

Se avete ottenuto timeout, non e' una tragedia. Significa soltanto: «non e' ancora pronto».

deferred: quando il lavoro non parte finché non richiedete il risultato

Questa parte sembra piccola, ma spiega un mucchio di «stranezze» nel comportamento del programma. Se il task viene creato con la policy std::launch::deferred, allora non e' obbligato a partire subito. Puo' avviarsi solo quando richiedete il risultato tramite wait() oppure get(). In quel caso wait_for() puo' restituire lo stato deferred.

Qui non entriamo nel dettaglio della scelta della policy di avvio, ma saper riconoscere deferred e' importante: altrimenti andrete a cercare «perché non c'e' parallelismo», mentre il colpevole e' semplicemente il contratto di avvio.

#include <future>
#include <chrono>
#include <iostream>

int main() {
    auto f = std::async(std::launch::deferred, [] { return 1; });

    auto st = f.wait_for(std::chrono::milliseconds{0});
    std::cout << (st == std::future_status::deferred) << "\n"; // 1

    std::cout << f.get() << "\n"; // 1
}

Qui il timeout nullo viene usato non come ciclo di attesa, ma come «domanda diagnostica»: «sei partito davvero o sei pigro?».

wait_until(): aspettare fino a una deadline concreta

A volte e' piu' comodo ragionare non in termini di «aspetta 200 ms», ma di «aspetta fino alle 12:00:05». Per questo esiste wait_until(time_point). Internamente e' sempre un'attesa, solo che il termine viene specificato come punto assoluto nel tempo, non come durata.

In pratica questo e' utile quando avete una deadline comune e non volete «accumulare» errori di attesa nei cicli.

Mini-esempio con deadline tramite steady_clock:

#include <future>
#include <chrono>
#include <thread>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::milliseconds{300});
        return 9;
    });

    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds{100};
    std::cout << (f.wait_until(deadline) == std::future_status::timeout) << "\n"; // 1
}

Nella maggior parte dei problemi da principianti wait_for() e' piu' semplice. Ma sapere che esiste anche la «versione con deadline» e' utile.

3. Esempio pratico: «calcolo asincrono» con spinner su wait_for()

Mettiamo insieme un mini-programma a partire da piccoli pezzi. L'idea e' semplice: avviamo un calcolo lungo tramite std::async, e in main controlliamo periodicamente la disponibilita' tramite wait_for(...) e stampiamo dei punti. E' simile a come le utility da console mostrano «sto lavorando...».

Funzione lenta: simuliamo il lavoro

Nella vita reale qui ci sarebbe il parsing di un file da gigabyte, una richiesta di rete o il calcolo di un modello. Nella vita didattica, invece, avremo «aspettare e restituire un numero», perché stiamo studiando future, non la sofferenza.

#include <chrono>
#include <thread>

int slow_add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds{700});
    return a + b;
}

Avvio e attesa con i punti

Il trucco principale: wait_for() in un ciclo. Non usiamo busy-wait con 0ms (e' una cattiva abitudine), ma diamo al sistema un po' di «respiro» con intervalli ragionevoli, per esempio 100ms.

#include <future>
#include <chrono>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, slow_add, 2, 3);

    while (f.wait_for(std::chrono::milliseconds{100}) != std::future_status::ready) {
        std::cout << "."; // ..... (mentre calcola)
    }

    std::cout << "\n" << f.get() << "\n"; // 5
}

Fate attenzione alla struttura: chiamiamo get() solo quando siamo certi che lo stato sia ready. Questo rende il comportamento del codice leggibile: «attendiamo con progresso → quando e' pronto, ritiriamo».

Versione ordinata con chrono_literals

Per rendere il codice piu' amichevole per gli occhi (e meno simile a contabilita' in millisecondi), si possono attivare i literal.

#include <future>
#include <chrono>
#include <iostream>

using namespace std::chrono_literals;

int main() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(500ms);
        return 42;
    });

    while (f.wait_for(100ms) != std::future_status::ready) std::cout << ".";
    std::cout << "\n" << f.get() << "\n"; // 42
}

Si', qui il while su una sola riga sembra un po' audace, ma per una dimostrazione e' accettabile. Nel codice reale e' meglio espandere il corpo del ciclo, se li' compare una logica piu' complessa di una sola operazione.

4. Errori tipici nel lavoro con std::future

Errore n. 1: chiamare get() due volte e aspettarsi che il future sia infinito.
std::future non e' una «scatola con copie del risultato», ma un accesso monouso allo shared state. Se volete usare il valore molte volte, ritiratelo una sola volta tramite get() e salvatelo in una variabile normale. Altrimenti prima o poi otterrete un errore di esecuzione e un'espressione molto triste.

Errore n. 2: pensare che timeout significhi «il task si e' rotto».
std::future_status::timeout dice soltanto che nel tempo assegnato il risultato non e' diventato pronto. Non e' una diagnosi né una sentenza, ma semplicemente «non e' ancora pronto». La reazione sana tipica e' aspettare ancora, mostrare all'utente il progresso oppure eseguire un lavoro alternativo.

Errore n. 3: scrivere while (f.wait_for(0ms) != ready) {} e chiamarlo con orgoglio «timeout».
Un ciclo del genere si trasforma in attesa attiva (busy-wait): il processore girera' come uno scoiattolo in ruota, e il beneficio sara' zero. Anche se vi serve «controllare spesso», impostate intervalli ragionevoli (per esempio 20ms–100ms) oppure progettate l'attesa in un altro modo.

Errore n. 4: ignorare lo stato deferred e sperare nel parallelismo.
Se il task e' stato creato come differito, allora wait_for() puo' restituire deferred, e non ci sara' alcun «lavoro in background» fino a get(). Di conseguenza, l'indicatore di progresso puo' sembrare buffo: state stampando punti, ma il lavoro non e' nemmeno iniziato.

Errore n. 5: catturare nell'attivita' asincrona riferimenti a variabili locali e poi stupirsi di comportamenti strani.
Anche se questo riguarda di piu' l'avvio del task, nel codice con future si manifesta in modo particolarmente spiacevole: aspettate get(), ma all'interno del task si accede a dati che sono gia' «morti». Se i dati servono al task, catturateli per valore oppure garantite una durata di vita degli oggetti piu' lunga di quella del lavoro asincrono.

1
Compito
C++ SELF, livello 71, lezione 1
Bloccato
Quadrato su richiesta
Quadrato su richiesta
1
Compito
C++ SELF, livello 71, lezione 1
Bloccato
Attesa dello stato
Attesa dello stato
1
Compito
C++ SELF, livello 71, lezione 1
Bloccato
Tre controlli
Tre controlli
1
Compito
C++ SELF, livello 71, lezione 1
Bloccato
Stato di due task
Stato di due task
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION