C++における非同期プログラミングは、現代のマルチコアCPUの性能を最大限に引き出すために不可欠な要素となっています。
特に、スレッド間で計算結果や状態を安全に受け渡す仕組みは、堅牢なアプリケーションを構築する上での鍵となります。
本記事では、C++11で導入され、現代のC++開発においても基盤となっているstd::futureとstd::promiseの使い方について、実例を交えて詳しく解説します。
std::promiseとstd::futureの基本概念
非同期処理において、あるスレッドで計算した値を別のスレッドへ送りたいケースは多々あります。
このような「スレッド間の値の受け渡し」を抽象化したものが、std::promiseとstd::futureのペアです。
この2つは、いわば「値を書き込むための口」と「値を読み取るための口」を結ぶパイプのような役割を果たします。
std::promiseとは何か
std::promiseは、値を「提供する側」が使用するオブジェクトです。
「将来、この値を設定することを約束する」という意味から、その名が付けられています。
プログラマは、非同期で行われる計算が完了したタイミングで、このpromiseに対して値をセットします。
std::futureとは何か
std::futureは、値を「受け取る側」が使用するオブジェクトです。
「将来、値が届く予定のチケット」のようなものだと考えると理解しやすいでしょう。
受け取り側は、値が準備できるまで待機したり、準備ができているかを確認したりすることができます。
共有状態(Shared State)の仕組み
std::promiseとstd::futureは、内部的に「共有状態」と呼ばれるメモリ領域を介して通信します。
promiseが値をセットすると、この共有状態に書き込まれ、futureがその値を読み取ります。
この仕組みにより、開発者はミューテックスや条件変数などを直接操作することなく、安全にデータを同期させることができます。
基本的な使い方と実装例
まずは、最もシンプルな数値の受け渡しを行うコードを見ていきましょう。
この例では、別スレッドで計算した結果をメインスレッドで受け取る処理を実装します。
#include <iostream>
#include <thread>
#include <future>
void calculate_square(std::promise<int> promiseObj, int value) {
// 重い処理をシミュレート
std::this_thread::sleep_for(std::chrono::seconds(1));
// 計算結果をセットする
int result = value * value;
promiseObj.set_value(result);
}
int main() {
// 1. promiseオブジェクトを作成
std::promise<int> p;
// 2. promiseに対応するfutureを取得
std::future<int> f = p.get_future();
// 3. 別スレッドを起動(promiseをムーブで渡す)
std::thread t(calculate_square, std::move(p), 10);
std::cout << "計算結果を待機中..." << std::endl;
// 4. 計算結果を受け取る(準備ができるまでブロックされる)
int finalResult = f.get();
std::cout << "結果: " << finalResult << std::endl;
t.join();
return 0;
}
計算結果を待機中...
結果: 100
実装のポイント
上記のコードにおいて、最も重要なのはstd::promiseはコピーできないという点です。
そのため、スレッドに関数を渡す際にはstd::move()を使用して所有権を移動させる必要があります。
また、f.get()を呼び出した際、もし計算が終わっていなければ、値がセットされるまでメインスレッドの実行は一時停止(ブロック)されます。
std::futureの主要なメソッド
std::futureには、単に値を受け取る以外にも便利な機能がいくつか用意されています。
これらを使いこなすことで、より柔軟な非同期処理の制御が可能になります。
get():値の取得
get()は、共有状態から値を取り出します。
このメソッドには重要な特徴があり、一度呼び出すとfutureオブジェクトは無効になるという性質を持っています。
二回以上同じ結果を取得したい場合は、後述するstd::shared_futureを検討する必要があります。
wait():準備完了まで待機
wait()は、値が準備されるまで現在のスレッドをブロックします。
get()とは異なり、値を取得するのではなく、単に「準備が整ったこと」を確認するために使用します。
wait_for() と wait_until():タイムアウト付き待機
無限に待機し続けるのを避けたい場合、タイムアウトを設定することができます。
wait_for()は「あと何秒待つか」を指定し、wait_until()は「いつまで待つか」という絶対時刻を指定します。
返り値としてstd::future_statusが返されるため、処理が完了したのかタイムアウトしたのかを判定できます。
| ステータス名 | 意味 |
|---|---|
| std::future_status::ready | 値の準備が完了しており、すぐに取得可能。 |
| std::future_status::timeout | 指定した時間内に準備が完了しなかった。 |
| std::future_status::deferred | 処理が遅延実行(std::asyncのフラグなどによる)されている。 |
例外処理の受け渡し
非同期処理の中でエラーが発生した場合、どのようにして呼び出し元のスレッドに伝えるべきでしょうか。
std::promiseには、値だけでなく「例外」をセットする機能も備わっています。
#include <iostream>
#include <thread>
#include <future>
#include <exception>
void task_with_error(std::promise<void> p) {
try {
// 何らかの異常を検知
throw std::runtime_error("非同期処理中にエラー発生!");
} catch (...) {
// 現在の例外をpromiseにセットする
p.set_exception(std::current_exception());
}
}
int main() {
std::promise<void> p;
std::future<void> f = p.get_future();
std::thread t(task_with_error, std::move(p));
try {
f.get(); // ここで再スローされる
} catch (const std::exception& e) {
std::cout << "メインスレッドで捕捉: " << e.what() << std::endl;
}
t.join();
return 0;
}
メインスレッドで捕捉: 非同期処理中にエラー発生!
このように、set_exception()を使用することで、別スレッドの例外をget()を呼び出したタイミングでキャッチできます。
これにより、スレッド間をまたいだエラーハンドリングが統一的かつ容易になります。
std::shared_futureによる複数スレッドへのブロードキャスト
標準的なstd::futureは、値を取得できるスレッドが一つだけに限定されています。
しかし、一つの計算結果を複数のスレッドで共有して読み取りたいケースもあります。
その際に使用するのがstd::shared_futureです。
shared_futureの特徴
std::shared_futureはコピーが可能です。
そのため、複数のスレッドに同じfutureを配布し、それぞれがget()を呼び出して値を受け取ることができます。
通常のfutureからshare()メソッドを呼ぶことで変換可能です。
#include <iostream>
#include <thread>
#include <future>
#include <vector>
int main() {
std::promise<int> p;
// std::futureをshared_futureに変換
std::shared_future<int> sf = p.get_future().share();
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
// 複数のスレッドにshared_futureをコピーして渡す
threads.emplace_back([sf, i]() {
std::cout << "スレッド " << i << " が待機中..." << std::endl;
std::cout << "スレッド " << i << " が受信: " << sf.get() << std::endl;
});
}
std::this_thread::sleep_for(std::chrono::seconds(1));
p.set_value(42); // 全スレッドに向けて値をセット
for (auto& t : threads) {
t.join();
}
return 0;
}
std::asyncとの使い分け
C++には、より高レベルな非同期APIとしてstd::asyncが存在します。
std::asyncは内部で自動的にpromiseとfutureを生成して管理してくれるため、非常に手軽です。
しかし、実務ではstd::promiseを手動で操作すべき場面が少なくありません。
どのような時にpromiseを使うべきか
std::asyncは、「今すぐこの関数を非同期で実行して、後で値をもらいたい」という関数単位の非同期化に向いています。
一方で、std::promiseはより低レイヤーな制御が可能です。
例えば、「コールバックベースの既存ライブラリからの通知をfutureで受け取りたい」場合や、「ネットワークのパケット受信を契機に値をセットしたい」といったケースです。
つまり、「いつ値が決まるかわからないが、決まった瞬間に通知したい」という汎用的なイベント通知機構として非常に優秀です。
注意点とベストプラクティス
promiseとfutureは強力ですが、誤った使い方をするとデッドロックやリソースリークの原因になります。
安全に使用するために、以下の点に注意しましょう。
値を一度しかセットできない
一つのstd::promiseに対してset_value()を二回呼び出すと、std::future_error例外がスローされます。
同様に、値をセットせずにpromiseオブジェクトが破棄されると、対応するfuture側では「壊れた約束(broken_promise)」という例外が発生します。
必ず、正常系でも異常系でも何らかの応答をpromiseに返すように設計してください。
ムーブセマンティクスの理解
前述の通り、std::promiseはムーブ専用のオブジェクトです。
スレッドに渡す際は、必ずstd::move()を使用して所有権がどこにあるかを明確にしましょう。
これにより、意図しない場所から値を書き込まれるリスクを低減できます。
有効性のチェック
futureを使用する際は、valid()メソッドでそのfutureがまだ共有状態を持っているかを確認できます。
特にget()を呼んだ後のfutureに対して再度操作を行わないよう、コードの構造には注意が必要です。
2026年における非同期処理の展望
C++20以降、コルーチン(Coroutines)が導入されたことで、非同期処理の書き方は大きく進化しました。
コルーチンはpromise/futureと似た概念を持っていますが、より軽量でスケールしやすいという特徴があります。
しかし、既存のコード資産や、単純なスレッド間の値受け渡しにおいては、依然としてstd::promiseとstd::futureが最も標準的で信頼できる選択肢です。
最新のライブラリやフレームワーク(std::executionなど)を使用する場合でも、背後にあるこれらの仕組みを理解しておくことは、デバッグやパフォーマンスチューニングにおいて大きなアドバンテージとなります。
まとめ
C++のstd::promiseとstd::futureは、非同期処理におけるデータ転送をシンプルかつ安全に行うための強力なツールです。
std::promiseが書き込み、std::futureが読み取りを担当し、両者が連携することで複雑なスレッド同期の記述を簡略化できます。
例外処理の伝搬やタイムアウト待機、さらにはstd::shared_futureによる多対一の通信など、実用的な機能が豊富に揃っています。
まずは基本的な数値の受け渡しから始め、徐々に例外処理やタイムアウト制御を取り入れていくことで、より洗練されたマルチスレッドプログラムを作成できるでしょう。
本記事で紹介した内容を参考に、現代的なC++の非同期プログラミングに挑戦してみてください。
