TopC++ 入門 › STEP 12 › 第67回 std::thread 入門

第67回 std::thread 入門 — C++11 で標準化された並行処理

C 時代は pthread_create や Windows の CreateThread を OS ごとに切り替えていましたが、C++11 からは std::thread で全環境同じコードになりました。ラムダをそのまま渡せるので書きやすさも段違いです。

最低限ここだけ

  • std::thread t([]{ ... }); で起動
  • t.join(); で終了を待つ
  • join 忘れは terminate

余裕があれば

  • join / detach / jthread の違い
  • 引数渡し(値・参照・std::ref)
  • hardware_concurrency / スレッドプール

1. まず触ってみる — ラムダを別スレッドで実行

スレッドとは「同時並行に動く実行ライン」のことです。1 つの CPU プログラムが、複数の足で同時に走り出すイメージ。std::thread関数(やラムダ)を渡すだけで、その関数が別のラインで走り始めます。

1-1. イメージ — キッチンに料理人が 2 人いる状態

main 関数という主人が料理してる横で、雇った料理人(スレッド)が別の料理を並行して作ってくれる。main は終わる前に「料理人の作業が終わるのを待つ(join)」必要があります。呼び戻さずに店を閉めると、料理人が迷子になって terminate する、というルールです。

1-2. 最小コード

hello_thread.cppC++11〜
#include <iostream> #include <thread> int main() { std::thread t([]{ std::cout << "hello from thread\n"; }); std::cout << "hello from main\n"; t.join(); // ← スレッド終了を待つ } // 出力順は保証されない(2 本の足が同時に走る)

ラムダを std::thread に渡すだけでスレッドが起動します。main が先に cout しても、スレッドの cout が先に出ても、どちらもあり得ます。これが並行実行です。

1-3. よくある質問

  • Q. 何個までスレッドを作れる? 技術的には数百〜数千。ただしCPU コア数を超えると競合で遅くなります。std::thread::hardware_concurrency() でコア数取得。
  • Q. join しないとどうなる? thread オブジェクトのデストラクタで std::terminate。忘れ対策に C++20 の jthread(後述)があります。
  • Q. メンバ関数を渡すには? std::thread t(&Obj::method, &obj, arg1)。第 1 引数がメンバポインタ、第 2 引数がthis相当。

2. 並行実行をタイムラインで見る

ボタンを押すと 3 本のスレッドを走らせる様子がタイムラインで見えます。main は最後に全員の join を待つので、画面上でも main の後半は「待ちの灰色帯」になります。

▶ 並行実行ビジュアライザ
スレッドの進行ログがここに表示されます。
並行実行で時間を稼げるのは、スレッド同士が CPU 待ち・I/O 待ちの「隙間」を埋め合うから。CPU をフルに使う計算だけなら、コア数を超えた並列化は速くなりません。

3. join / detach / jthread

std::thread を終わらせる方法は 3 種類あります。違いは「main が待つか、スレッドに任せるか、自動で待つか」だけ。

方式意味おすすめ度
t.join()スレッドの終了を待つ(ブロック)◎ 基本これ
t.detach()スレッドを切り離して放置。終了は監視できない△ 用途限定
std::jthread(C++20)デストラクタで自動 join + キャンセル対応◎ C++20 以降の第一候補
join_vs_jthread.cppC++20
// 従来: join を忘れたら terminate { std::thread t(doWork); // ... 途中で throw したら join されない! t.join(); } // C++20: jthread が自動で join { std::jthread t(doWork); } // ← スコープ抜けで自動 join(RAII) // キャンセル(協調的停止) std::jthread t([](std::stop_token st) { while (!st.stop_requested()) { ... } }); t.request_stop(); // スレッドに停止要請

3-1. detach は基本使わない

detach したスレッドはプログラム終了時にまだ生きていても強制終了。共有リソースの参照が寿命切れを起こしやすく、バグの温床になります。どうしても使うなら「プログラムが終わっても継続してほしいログ出力」など限定的な用途に。

4. 引数渡しの落とし穴

std::thread に渡した引数は常にコピーされてからスレッドで使われます。参照で渡したいときは明示的に std::ref を使わないと、コピーされた別オブジェクトが触られます。

by_value.cppNG
void add(int& x) { x += 1; } int main() { int n = 0; std::thread t(add, n); // ← n のコピーが渡る t.join(); std::cout << n; // 0(変わらない!) }
by_ref.cppOK
void add(int& x) { x += 1; } int main() { int n = 0; std::thread t(add, std::ref(n)); // ← 参照を明示 t.join(); std::cout << n; // 1 }

4-1. ローカル変数への参照キャプチャに注意

ラムダでローカル変数を参照キャプチャ [&] したまま detach すると、関数が終わった瞬間に参照先が消えます。

dangling.cppUB
void spawn() { int local = 42; std::thread t([&]{ // local を参照キャプチャ std::this_thread::sleep_for(1s); std::cout << local; // ← local は既に消滅 }); t.detach(); // 待たずに関数終了 } // ← local 死亡

この手のバグは AddressSanitizer で検出できます(第 74 回参照)。スレッドが見る変数は必ずスレッドより長生きさせるのが原則です。

5. C の pthread と比較

pthread.cC: pthread
void* worker(void* arg) { int* p = (int*)arg; printf("n=%d\n", *p); return NULL; } pthread_t tid; int n = 42; pthread_create(&tid, NULL, worker, &n); pthread_join(tid, NULL); // void* にキャストが必要 // Windows では CreateThread に書き換え
thread.cppC++: std::thread
std::thread t([n]{ std::cout << "n=" << n; }); t.join(); // ・ラムダで閉じ込められる // ・キャスト不要 // ・型安全(コンパイル時に引数チェック) // ・Windows/Linux/Mac 同じコード

std::thread は内部で pthread (Linux/Mac) / Win32 (Windows) を呼んでいるだけなので、性能は変わりません。書きやすさとクロスプラットフォーム性のみが違います。

6. 実務の注意

  • スレッドは安くない。作成に数万命令。大量作成ならスレッドプールを使う(std::async 第 60 回 / ライブラリで BS::thread_pool など)。
  • 共有変数には mutex / atomic。裸で書き込むとデータ競合 = UB(次回 59 回で扱う)。
  • 例外はスレッド境界を越えない。スレッド内で throw されて catch されないと terminate。スレッド関数の外枠で try/catch を張る or std::async で future 経由で受け取る。
  • C++20 なら jthread。手動 join は忘れやすい。
  • printf / cout の出力は混ざる。ログは mutex で守るか、専用ロガーを使う。

7. 理解度チェック

4 問。

Q1. std::thread のデストラクタが呼ばれた時、まだ join/detach されていないとどうなる?

自動で join される
std::terminate で強制終了
スレッドは放置され main は継続
C++11 の std::thread は自動 join しない設計で、join / detach 忘れは terminate。C++20 の jthread なら自動 join される。

Q2. 参照で引数を渡したいときに必要なのは?

関数シグネチャを int& にすれば自動で参照渡し
std::ref() でラップして明示する
ポインタで渡す以外は不可能
std::thread は引数をコピーするので、参照先を生かしたいなら std::ref(n) で明示が必要。これは C++ の FAQ 級の罠。

Q3. hardware_concurrency() は何を返す?

利用可能なハードウェア並列実行ユニット(コア数目安)
現在動いているスレッド数
最大作成可能なスレッド数
論理コア数のヒント。0 が返ることもある(その時は 1 など安全な値にフォールバック)。スレッドプールのサイズ決定に使う。

Q4. 次の書き方で危ういのは?

void run() {
  std::string s = "hello";
  std::thread t([&]{ process(s); });
  t.detach();
}
t が未初期化である
detach がそもそも不正
run が返った後に s が消滅し、スレッドがダングリング参照を触る
detach + スタック変数の参照キャプチャは典型的な UB コンボ。detach するならキャプチャは値でコピー、または共有所有権(shared_ptr)にする。