concurrency — фундаментconcurrency — fundamentals
Это не альтернатива мьютексам и 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.
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, потока и передачи результата — он запускает вызываемый объект (возможно, в новом потоке, а возможно и нет — зависит от 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, полученного из 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.
Повторный вызов 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.
Если код полагается на реальный параллелизм (например, для сокрытия задержки 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 делает их неподходящими для сценария "производитель непрерывно поставляет значения потребителю" — для этого нужна очередь с синхронизацией (мьютекс + 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).