TopC++ 入門 › 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::asyncdeferred の違い
  • promise / packaged_task の使い分け
  • wait_for によるタイムアウト付き待機

1. まず触ってみる — 戻り値を受け取る最短コード

std::thread で結果を受け取るには、共有変数 + mutex か、参照で出力引数を渡す必要があって面倒でした。std::async は関数を非同期で呼び出し、結果の引換券(future) を返します。

1-1. イメージ — 宅配便の受け取り票

ラーメン屋で番号札を受け取って、席で待って、呼ばれたら引き換え。これが future の使い方。「頼んだ」「待つ」「受け取る」の 3 段階が分離しているので、間に別の仕事を挟めます。

1-2. 最小コード

hello_async.cppC++
#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::asyncstd::future<T> を返す。T は compute の戻り値型(int)。
  • f.get() は結果が出るまでブロックする(完了していれば即座に返る)。
  • get は 1 回しか呼べない(2 回目は UB)。何度も見るなら shared_future

1-3. 例外もそのまま伝わる

exc.cppC++
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 ライフサイクル

呼び出し側スレッド

非同期タスク

future の状態がここに表示されます。

2-1. wait_for でポーリング

poll.cppC++
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.cppC++
// 必ず並行実行したい場合は明示する 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.cppC++
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.cppC++
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.cppC++
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.cppC++
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_poolfolly::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(...) を左辺に受けずに式として捨てると、その場で待ってしまうトラップ。