Top ›
C++ 入門 › STEP 12 › 第69回 std::async と std::future
第69回 std::async と std::future — 戻り値を受け取れる非同期
std::thread は起動できても戻り値を受け取れない のが弱点でした。std::async は結果の引換券 std::future を返し、後で .get() するだけで値が取れます。例外もそのまま伝わります。
最低限ここだけ
auto f = std::async(fn, args...);
f.get(); で結果を待って受け取る
例外も自動で伝わる
余裕があれば
launch::async と deferred の違い
promise / packaged_task の使い分け
wait_for によるタイムアウト付き待機
このページの流れ
1. まず触ってみる — 戻り値を受け取る最短コード
2. future の 3 状態を見る
3. launch::async と launch::deferred
4. 並列計算の典型パターン
5. promise と packaged_task
6. 実務の注意
7. 理解度チェック
1. まず触ってみる — 戻り値を受け取る最短コード
std::thread で結果を受け取るには、共有変数 + mutex か、参照で出力引数を渡す必要があって面倒でした。std::async は関数を非同期で呼び出し、結果の引換券(future) を返します。
1-1. イメージ — 宅配便の受け取り票
ラーメン屋で番号札を受け取って、席で待って、呼ばれたら引き換え。これが future の使い方。「頼んだ」「待つ」「受け取る」 の 3 段階が分離しているので、間に別の仕事を挟めます。
1-2. 最小コード
hello_async.cpp C++
#include <future>
#include <iostream>
int compute (int x) {
std::this_thread::sleep_for (2s);
return x * x;
}
int main () {
std::future<int > f = std::async (compute, 7 );
std::cout << "他の仕事...\n" ; // 計算中でも動ける
int r = f.get (); // ← 結果を受け取る(必要なら待つ)
std::cout << "result = " << r; // 49
}
ポイント 3 つ:
std::async は std::future<T> を返す。T は compute の戻り値型(int)。
f.get() は結果が出るまでブロック する(完了していれば即座に返る)。
get は 1 回しか呼べない (2 回目は UB)。何度も見るなら shared_future。
1-3. 例外もそのまま伝わる
exc.cpp C++
int bad () { throw std::runtime_error("oops" ); }
auto f = std::async (bad);
try {
f.get ();
} catch (const std::exception& e) {
std::cout << e.what (); // "oops"
}
// ↑ スレッド境界を越えて例外が伝播する
// これが std::thread では起きない(投げたら terminate)
これが async の大きな利点。thread では例外は境界で消えてしまいますが、async は future に格納されて get() で再投げされます。
2. future の 3 状態を見る
future は内部に共有状態 を持ち、以下の 3 状態を遷移します。
▶ future ライフサイクル
async(compute) を実行
リセット
future の状態がここに表示されます。
2-1. wait_for でポーリング
poll.cpp C++
auto f = std::async (compute);
while (f.wait_for (100ms) != std::future_status::ready) {
drawProgress(); // UI 更新など
}
auto result = f.get ();
wait_for は「指定時間だけ待つ」。ready / timeout / deferred の 3 状態が返るので、UI ループと相性が良い。
3. launch::async と launch::deferred
std::async(fn) はどのスレッドで走るかを実装に任せる モード。明示的に指定すると挙動が決まります。
ポリシー 動作 使うタイミング
std::launch::async必ず新しいスレッド で実行 確実に並行させたい時
std::launch::deferredget() された時に呼び出しスレッドで 実行(lazy) 「必要になったら計算」
指定なし(既定) 実装任せ。async か deferred を選ぶ 非推奨 : 並列にならない可能性あり
policy.cpp C++
// 必ず並行実行したい場合は明示する
auto f = std::async (std::launch::async, compute, 7 );
// 遅延評価(呼ばれないかもしれない重い計算)
auto g = std::async (std::launch::deferred, []{
return expensiveCalc();
});
if (needed) auto v = g.get (); // この時点で実行
既定の落とし穴 : 既定ポリシーだと deferred を選ばれる可能性があり、get() までスレッドが立たない。並行性能を狙うなら launch::async を明示しましょう。
4. 並列計算の典型パターン
4-1. タスクを 4 本に分割して合計
parallel_sum.cpp C++
long long sum_range (const std::vector<int >& v, int lo, int hi) {
long long s = 0 ;
for (int i=lo; i<hi; ++i) s += v[i];
return s;
}
std::vector<int > v(10'000'000 , 1 );
const int N = 4 , Q = v.size () / N;
std::vector<std::future<long long >> fs;
for (int i=0 ; i<N; ++i)
fs.push_back (std::async (std::launch::async,
sum_range, std::cref (v), i*Q, (i+1 )*Q));
long long total = 0 ;
for (auto & f : fs) total += f.get ();
// 10M 要素の合計を 4 並列で。CPU コア 4 以上なら ~4 倍速
巨大データを等分 → 各パートを async → 最後に get して合計。map-reduce の素朴版 です。
4-2. 複数のネットワーク呼び出しを並行に
fetch.cpp C++
auto a = std::async (std::launch::async, fetchUser, id);
auto b = std::async (std::launch::async, fetchPrefs, id);
auto c = std::async (std::launch::async, fetchAds, id);
auto page = buildPage(a.get (), b.get (), c.get ());
// 3 つが並行に走るので、合計待ち時間は max(三者) に近い
5. promise と packaged_task
async ほど簡単ではないが、もう少し細かい制御 が欲しい時に使います。
5-1. std::promise — 自分で値を設定する
promise.cpp C++
std::promise<int > p;
std::future<int > f = p.get_future ();
std::thread t([&p]{
std::this_thread::sleep_for (1s);
p.set_value (42 ); // ← 好きなタイミングで設定
});
std::cout << f.get (); // 42
t.join ();
// async と違い、どのスレッドで設定してもよい
// コールバック的なパターンに使える
5-2. std::packaged_task — 関数に future を紐付ける
packaged.cpp C++
std::packaged_task<int (int )> task(compute);
std::future<int > f = task.get_future ();
// task を別スレッド / スレッドプールに渡せる
std::thread(std::move (task), 7 ).detach ();
int r = f.get (); // 49
// スレッドプール実装でよく使われる
6. 実務の注意
① future のデストラクタがブロックする (launch::async のとき)。スコープ終端で待たされるので、無視した future は落とさないこと。
② get() は 1 回だけ 。2 回目は UB。複数箇所で共有したいなら std::shared_future。
③ large なラムダキャプチャは参照で渡さない 。thread と同じくダングリング参照の原因。
④ async は簡便だが柔軟性が低い 。本格的なスレッドプールが必要なら BS::thread_pool や folly::Executor などを検討。
⑤ C++20 の coroutines とは別物 。coroutines は文法レベルの非同期で、ランタイムに依存しない。ここでは扱わないが C++ モダンの主流はコルーチンへ移行中。
7. 理解度チェック
4 問。
Q1. std::async 内で例外が投げられた時、どこで受けられる?
非同期タスク内の try/catch
future.get() を呼んだ側の try/catch
どこでも受けられない(terminate)
例外は future に格納され、get() で再投げされる。これが std::thread との決定的な違い。
Q2. std::async を引数なしで呼んだとき、必ず並行実行されるか?
必ず別スレッドで並行実行される
実装依存。deferred になる可能性もある
必ず deferred(遅延実行)になる
既定ポリシーは async | deferred で、実装が選ぶ。並行させたいなら std::launch::async を明示。
Q3. future.get() を同じオブジェクトに 2 回呼ぶと?
2 回とも同じ値が返る
2 回目は未定義動作
2 回目は nullopt 相当が返る
future は 1 度きり。複数回読む必要があるなら shared_future を使う。
Q4. std::async 由来の future を "投げっぱなし" にすると?
タスクは完全に独立して動き続ける
future の破棄時にデストラクタがタスク終了を待つ(ブロック)
即座にキャンセルされる
launch::async で作った future はデストラクタがブロック待ちをする(規格の特例)。std::async(...) を左辺に受けずに式として捨てると、その場で待ってしまうトラップ。