Skip to content

100 Advanced The Hundred

The async That Runs in Series

fire and forget, minus the forget

using namespace std::chrono;

void upload() { std::this_thread::sleep_for(milliseconds(200)); }

long elapsed_ms(steady_clock::time_point t0) {
    return duration_cast<milliseconds>(steady_clock::now() - t0).count();
}

int main() {
    auto t0 = steady_clock::now();
    (void)std::async(std::launch::async, upload);   // fire...
    (void)std::async(std::launch::async, upload);   // ...and forget
    std::cout << "fire-and-forget: " << elapsed_ms(t0) << " ms\n";

    t0 = steady_clock::now();
    auto a = std::async(std::launch::async, upload);
    auto b = std::async(std::launch::async, upload);
    a.wait();
    b.wait();
    std::cout << "futures kept:    " << elapsed_ms(t0) << " ms\n";
}

Run it. What do the two timings say?

Answer

Roughly fire-and-forget: 400 ms — the "forgotten" launches ran one after the other, and main waited at each semicolon. The kept futures overlap: roughly 200 ms.

Why

The future returned by std::async is special: as the last handle to the task's shared state, its destructor blocks until the task finishes. Discard the return value and that future is a temporary, dead at the semicolon — each "background" launch waits right there, mid-statement, and the two uploads run in series: 200 + 200 ≈ 400 ms. The design is deliberate (a detached task could outlive locals it references), but it turns a bare std::async(...) into a slow synchronous call. libstdc++ marks std::async [[nodiscard]], so plain discarding earns "ignoring return value ... declared with attribute 'nodiscard'" (-Wunused-result, on even without -Wall) — the (void) cast above is the customary silencer, wait included. Only std::async futures do this; one from a std::promise or std::packaged_task destroys without waiting.

The fix

Name the futures — the join moves to end of scope, and the tasks truly overlap:

auto f1 = std::async(std::launch::async, upload);   // named → runs in parallel
auto f2 = std::async(std::launch::async, upload);   // dtors join at scope exit
std::thread(upload).detach();   // no result wanted? skip async (or C++20 std::jthread)

Takeaway: std::async's future is an RAII join — drop it on the floor and you wait on the spot.

Try it: g++ -std=c++17 -pthread main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry