concurrency — фундаментconcurrency — fundamentals

std::future // std::promise // std::async

Это не альтернатива мьютексам и condition_variable — это готовая абстракция ровно для одного частого случая: "один поток передаёт единственный результат (или исключение) другому потоку ровно один раз". Внутри всё равно используется синхронизация, но она спрятана от вызывающего кода.This isn't an alternative to mutexes and condition_variable — it's a ready-made abstraction for exactly one common case: "one thread hands a single result (or exception) to another thread, exactly once." Internally it still uses synchronization, but that's hidden from the calling code.

C++11C++11 частый вопросcommon interview question

promise/future: одноразовый канал передачи значенияa one-shot value channel

std::promise<T> и std::future<T> — две половины одного разделяемого состояния. Поток-производитель держит promise и вызывает set_value() (или set_exception()) ровно один раз. Поток-потребитель держит future, полученный через promise.get_future(), и вызывает get(), который блокируется, пока значение не будет установлено, а затем возвращает его (или пробрасывает исключение, если было установлено оно).std::promise<T> and std::future<T> are two halves of one shared state. The producer thread holds the promise and calls set_value() (or set_exception()) exactly once. The consumer thread holds the future obtained via promise.get_future(), and calls get(), which blocks until the value is set, then returns it (or rethrows the exception, if that's what was set).

std::promise<int> prom;
std::future<int> fut = prom.get_future();

std::thread worker([&prom] {
    try {
        int result = expensiveComputation();
        prom.set_value(result);
    } catch (...) {
        prom.set_exception(std::current_exception());  // исключение переносится в поток-потребитель// the exception is carried over to the consumer thread
    }
});

int value = fut.get();   // блокируется до set_value/set_exception; повторный вызов get() — UB// blocks until set_value/set_exception; calling get() again is UB
worker.join();

Ключевое отличие от условной переменной: get() можно вызвать РОВНО ОДИН РАЗ за время жизни future — это "одноразовый" канал, а не многократно переиспользуемый механизм ожидания, как condition_variable.The key difference from a condition variable: get() can be called EXACTLY ONCE over a future's lifetime — it's a "one-shot" channel, not a repeatedly reusable waiting mechanism like condition_variable.

std::async: promise/future без ручной проводкиpromise/future without manual wiring

std::async избавляет от ручного создания promise, потока и передачи результата — он запускает вызываемый объект (возможно, в новом потоке, а возможно и нет — зависит от policy) и сразу возвращает future на его результат.std::async removes the need to manually create a promise, a thread, and wire through the result — it launches a callable (possibly on a new thread, possibly not — depending on the policy) and immediately returns a future for its result.

int compute(int x) { return x * x; }

std::future<int> fut = std::async(std::launch::async, compute, 21);  // гарантированно новый поток// guaranteed new thread
int result = fut.get();  // 441// 441

Два launch policy кардинально меняют поведение: std::launch::async гарантирует выполнение в отдельном потоке немедленно; std::launch::deferred откладывает выполнение до первого вызова get()/wait(), выполняя код СИНХРОННО в том же потоке, что вызвал их — то есть без параллелизма вообще. Вызов std::async(compute, 21) без явной policy оставляет выбор реализации, что на практике — источник недетерминированного поведения между платформами/компиляторами.The two launch policies drastically change behavior: std::launch::async guarantees execution on a separate thread immediately; std::launch::deferred postpones execution until the first call to get()/wait(), running the code SYNCHRONOUSLY on whichever thread calls them — meaning no parallelism at all. Calling std::async(compute, 21) without an explicit policy leaves the choice up to the implementation, which in practice is a source of non-deterministic behavior across platforms/compilers.

Скрытая ловушка: деструктор future от async блокируетсяThe hidden trap: an async future's destructor blocks

У обычного future, полученного из promise напрямую, деструктор не блокирует — если поток-потребитель никогда не вызвал get(), объект просто разрушается. Но future, полученный от std::async с политикой launch::async (или неопределённой), — особый случай: его деструктор БЛОКИРУЕТСЯ, ожидая завершения асинхронной задачи, если get()/wait() не были вызваны заранее. Это по сути неявный `join()`, легко упускаемый из виду.An ordinary future obtained directly from a promise has a non-blocking destructor — if the consumer thread never called get(), the object is simply destroyed. But a future obtained from std::async with the launch::async policy (or an unspecified one) is a special case: its destructor BLOCKS, waiting for the async task to finish, if get()/wait() weren't called beforehand. It's effectively an implicit `join()`, easy to overlook.

{
    auto fut = std::async(std::launch::async, slowTask);
    // fut не используется дальше// fut is never used further
}   // деструктор fut здесь блокирует текущий поток до завершения slowTask — "невидимая" синхронная точка// fut's destructor here blocks the current thread until slowTask finishes — an "invisible" sync point

Практическое следствие: запуск нескольких std::async-задач подряд БЕЗ сохранения всех future заранее и последующего параллельного get() у каждого превращает код в фактически последовательный — каждый временный future блокируется в своём деструкторе до перехода к следующему.The practical consequence: launching several std::async tasks in a row WITHOUT holding onto every future upfront and later calling get() on each in parallel effectively turns the code sequential — each temporary future blocks in its destructor before moving on to the next.

ГраблиGotchas

get() можно вызвать только один разcan only be called once

Повторный вызов get() на том же future — undefined behavior (обычно бросает std::future_error на большинстве реализаций, но полагаться на это нельзя). Если результат нужен нескольким потребителям — std::shared_future, допускающий множественные get().Calling get() again on the same future is undefined behavior (usually throws std::future_error on most implementations, but that can't be relied on). If several consumers need the result — use std::shared_future, which allows multiple get() calls.

Забытая проверка launch policy — молчаливая потеря параллелизмаForgetting to check the launch policy silently loses parallelism

Если код полагается на реальный параллелизм (например, для сокрытия задержки I/O за вычислениями), но std::async вызван без std::launch::async, реализация вправе выбрать deferred и выполнить всё синхронно внутри get() — код останется корректным, но полностью потеряет ожидаемое ускорение, без единой ошибки компиляции или рантайма, сигнализирующей об этом.If the code relies on genuine parallelism (e.g., to hide I/O latency behind computation), but std::async is called without std::launch::async, the implementation is free to pick deferred and run everything synchronously inside get() — the code stays correct but loses the expected speedup entirely, with no compile or runtime error signaling it.

future/promise — не для потокового (многократного) обмена даннымиfuture/promise isn't for streaming (repeated) data exchange

Одноразовая природа future/promise делает их неподходящими для сценария "производитель непрерывно поставляет значения потребителю" — для этого нужна очередь с синхронизацией (мьютекс + condition_variable, или lock-free очередь), а не повторное создание пар promise/future на каждое значение (что технически возможно, но избыточно дорого).The one-shot nature of future/promise makes them unsuitable for a "producer continuously feeds values to a consumer" scenario — that needs a synchronized queue (mutex + condition_variable, or a lock-free queue), not repeatedly creating a new promise/future pair per value (technically possible, but needlessly expensive).