コンテンツにスキップ

最終更新:2026-10-12

C++ std::thread と jthread

thread と jthread の全面解析

1.準備

本記事の内容はgcc-13のソースコードに基づいて展開します。C++11 std::threadの使い方と内部実装を詳しく解説し、pthread + threadを混在させる方法、そしてC++20 jthreadの使い方と内部実装を解説します。

あなたがマルチスレッド開発の初心者でもベテランでも、本記事はどちらの方にとっても学びに適しています。基礎的な理論知識も含まれますし、豊富な実践と内部コードの解明も含まれます。

ソースコード分析のバージョン:gcc-13

https://github.com/gcc-mirror/gcc/tree/releases/gcc-13

目次は以下のとおりです:

2.C++11のthread

2.1 基本的な使い方

threadを作成し、自分の実行関数や引数などを渡します。

ここのresultにはatomicを使うこともできれば、promiseを使うこともできます。

基本的な使い方では、一般にt.joinに注意する必要があります。これによりメインスレッドが子スレッドを待機するようになります。join()を呼び出したスレッドの実行が完了するまで、現在のスレッドをブロックします。

std::atomic<int> result(0);

void threadFunction(int a, int b) {
    result = a + b;
    std::cout << "Child thread finished its work.\n";
}

int main() {
    int a = 5;
    int b = 7;

    std::thread t(threadFunction, a, b);

    t.join();
    std::cout << "Result computed by the child thread: " << result << std::endl;

    return 0;
}

もしスレッドに対してjoin()やdetach()を指定しなかった場合、実行時エラーが発生します。メイン/現在のスレッドの実行は完了するのに、作成した他のスレッドがまだ実行中だからです。

例:上のt.join()を取り除くと、プログラムはクラッシュします。

Result computed by the child thread: 0
Child thread finished its work.
terminate called without an active exception
[1]    52814 abort      ./a.out

2.2 STL実装

ぜひstd_thread.hファイルを開いて、私と一緒に内部実装を読んでみてください。

コードのパス:gcc/libstdc++-v3/include/bits/std_thread.h

2.2.1 pthreadとの関連

まずこれはthreadというクラスであり、次に比較的重要な型が1つあります。私のLinuxシステムではこのマクロは有効になっていて、デフォルトでは一般に__gthread_tであり、これは通常pthread_tです。

#ifdef _GLIBCXX_HAS_GTHREADS
    using native_handle_type = __gthread_t;
#else
    using native_handle_type = int;
#endif

すなわち:

typedef pthread_t __gthread_t;

そこで最初の結論が得られます:内部実装はLinuxのpthreadに基づいています。

2.2.2 内部id

threadクラスには、_M_idというプライベートメンバがあります。

private:
    id              _M_id;

その型は内部で定義されたクラスであり、さらに中を掘り下げるとnative_handle_typeです。

class id
{
  native_handle_type    _M_thread;

public:
  id() noexcept : _M_thread() { }

  explicit
  id(native_handle_type __id) : _M_thread(__id) { }

}

上の内容と組み合わせると、次のことが分かります:

private:
    pthread_t               _M_id;

お馴染みではないでしょうか。2つ目の知識ポイントが得られました:threadの内部ではidを保持しており、Linux上ではデフォルトでpthread_tです。

2.2.3 joinableかどうか

あるスレッドが「joinable」状態にある場合、その実行が完了する前に、プログラムはjoin()関数を呼び出してそのスレッドの実行完了を待つことができます。

joinableの判定基準は次のとおりです:

_M_id != id()

前の内容に基づいて、次のように導出できます:

pthread_t _M_id;
pthread_t null_thread = pthread_t();
_M_id != null_thread;

すなわち:joinableの判定基準は、現在のスレッドがデフォルトのスレッドid(0)と等しくないかどうかです。

joinメソッドは非常に重要で、私たちは普段よく使う必要があります。その実装原理を簡単に言うと次のとおりです:

  • joinableかどうか
  • 可能なら、pthread_joinを呼び出します(ここのgthreadは実際にはpthreadのインターフェースを呼び出します)
  • 不可能、または上のjoinでエラーがあった場合、EINVALあるいはjoinのエラーを例外として投げます。
  • 非joinableに設定します。すなわち:内部のスレッドidを0に設定します
thread::join()
{
  int __e = EINVAL;

  if (_M_id != id()) // 是否可joinable
    __e = __gthread_join(_M_id._M_thread, 0);

  if (__e)
    __throw_system_error(__e);

  _M_id = id(); // _M_id = pthread_t(),设置为0
}

joinした後にもう一度joinすると、上記のEINVAL例外、すなわち無効な引数の例外が投げられます。

terminate called after throwing an instance of 'std::system_error'
  what():  Invalid argument

2.2.4 デタッチ

joinと似ていますが、逆の操作です。プログラムに対してもうそのスレッドの実行を気にしないと伝えます。実装ロジックは、呼び出すインターフェースが異なることを除いて他はすべて同じなので、詳しい説明は省略します。

detachで使われているのはpthread_detachです。

void
thread::detach()
{
  int __e = EINVAL;

  if (_M_id != id())
    __e = __gthread_detach(_M_id._M_thread);

  if (__e)
    __throw_system_error(__e);

  _M_id = id();
}

2.2.5 構築と破棄

2.2.5.1 構築

使用例:

thread t1;

デフォルト構築の内部コード:

thread() noexcept = default;

引数付き構築の使用例:

std::thread t2(threadFunction, a, b);

引数付き構築の内部コード:

template <typename _Callable, typename... _Args,
          typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable &&__f, _Args &&...__args) {}

内部実装は次のとおりです:

_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(
                std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
            _M_thread_deps_never_run);

核心となる関数は_M_start_threadだということが分かります。ここのコードを簡略化すると次のとおりです:

const int err = __gthread_create(&_M_id._M_thread,
         &execute_native_thread_routine,
         state.get());
if (err)
  __throw_system_error(err);

__gthread_createはpthread_createと捉えることができます。これで非常によく分かったのではないでしょうか?

ここで1つ注意すべき細かい点は、pthread_createに渡す引数のうちfuncはexecute_native_thread_routineだということです。上記をpthread_createに変換すると次のようになります:

pthread_create (&_M_id._M_thread, NULL, &execute_native_thread_routine, state.get());

そしてexecute_native_thread_routineはextern "C"で包裹された関数であり、C++の関数名にCリンケージを持たせています。

以上よりコンストラクタのロジックは非常に明確です。pthread_createを呼び出して内部のスレッドidを初期化しているだけです。しかし、ここで1つの疑問があります。ユーザーが渡した関数と引数はどのように処理されるのでしょうか?

ここでStateの登場です。注意深い方は、上のpthread_createの最後の引数が通常の関数の引数であるstate.get()になっており、関数はexecute_native_thread_routineであることに気づいたでしょう。

static void*
execute_native_thread_routine(void* __p)
{
  thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
  __t->_M_run();
  return nullptr;
}

_Stateはインターフェースであり、その実装は_State_implです。引数と関数はここへ転送され、_M_runを通じてユーザーが渡した関数を実行することが分かります。実行時には_Callableのoperator()が呼び出されます。先ほど、作成時には_Wrapperが付いていることを私たちは知っています。したがって_Callableは_Wrapperと等しく、__Wrapperはさらに_Call_wrapperの別名であり、ユーザーの関数と引数をここへ渡します。

using _Wrapper = _Call_wrapper<_Callable, _Args...>;

struct _State
{
  virtual ~_State();
  virtual void _M_run() = 0;
};
using _State_ptr = unique_ptr<_State>;


template <typename _Callable>
struct _State_impl : public _State
{
  _Callable _M_func;

  template <typename... _Args>
  _State_impl(_Args &&...__args)
      : _M_func(std::forward<_Args>(__args)...)
  {
  }

  void
  _M_run() { _M_func(); }
};

ということで、ここで次のコードを見てみましょう。

template <typename... _Tp>
using _Call_wrapper = _Invoker<tuple<typename decay<_Tp>::type...>>;

これは、上の_Call_wrapperのすべての引数から参照と修飾を取り除いたものをtupleとして渡すことに相当します。すなわち:

_Invoker<tuple<_Callable, Arg1, Arg2, ...>

そして最終的にstd::__invokeへ渡されます。

したがって、もし次のような1行のコードを書いたとすると:

_Call_wrapper<decltype(&add), int, int> invoker(&add, 3, 4);

これはinvokeを通じてadd関数を呼び出してくれます。

invoke(_Fn&& __f, _Args&&... __args)

もちろん上で呼び出されているのはその内部の__invokeであり、両者に違いはありません。1つは外部向けのインターフェースで、1つは内部向けのものです。

以上の解説を経て、小まとめをしてみましょう:

  • 引数付きコンストラクタはpthread_createを通じてtidを作成します
  • pthread_createに渡される関数は包裹された関数であり、内部ではStateを使って駆動します
  • ユーザーは関数と引数をthreadに渡し、最終的にこの関数の呼び出しはstd::__invokeによって駆動されます。
2.2.5.2 コピーとムーブ

threadはコピーコンストラクタとコピー代入を禁止しており、ムーブコンストラクタとムーブ代入のみをサポートします。

thread(const thread &) = delete;
thread &operator=(const thread &) = delete;

ムーブコンストラクタ

thread(thread &&__t) noexcept
{
  swap(__t);
}

void
swap(thread &__t) noexcept
{
    std::swap(_M_id, __t._M_id);
}

ムーブ代入

thread &operator=(thread &&__t) noexcept
{
  if (joinable())
    std::__terminate();
  swap(__t);
  return *this;
}
2.2.5.3 破棄

破棄の原則は、自分が作成したスレッドのリソースを消費/解放しなければならないというものです。スレッドがjoinableである場合、それはpthread_createされましたが、まだ使用されていないことを意味し、このときは直接terminateします。

~thread()
{
  if (joinable())
    std::__terminate();
}

2.2.6 その他のメンバ

例えば:スレッド内部のスレッドidの取得、スレッドのid内部の_M_threadの取得です。

id
get_id() const noexcept
{ return _M_id; }

native_handle_type
native_handle()
{ return _M_id._M_thread; }

2.2.7 面接でのポイント

1.構築と破棄

ここで1つ質問です:次のコードには何か問題がありますか?

// test1.cc
int main() {
  thread t1;
}

// test2.cc
int main() {
    std::thread t2(threadFunction, a, b);
}

その答えは:test1は正常に実行され、test2はcrashします!

原因は:threadのデフォルトコンストラクタは何もしないのに対し、引数付きコンストラクタの中では__gthread_create(すなわちpthread_create)が呼び出されてスレッドが作成され、内部の_M_id._M_threadが初期化されます。破棄のときにjoinableかどうかが判断され、このときの_M_idはid()と等しくない、すなわちjoinable = trueなので、直接std::__terminate()が呼び出され、crashします。

2.ムーブ代入

次の2つの関数f1、f2があるとします。この2つの関数は正常に実行できるでしょうか?その理由は?

void f1() {
  std::thread t1(tfunc);
  std::thread t2;
  t2 = std::move(t1);
  t2.join();
}
void f2() {
  std::thread t1(tfunc);
  std::thread t2(tfunc);
  t2 = std::move(t1);
  t2.join();
}

その答えは:f1は正常に実行され、f2はcrashします!

原因は:f1ではt2がデフォルトコンストラクタを使用しており(何もしません)、したがってt2自体は非joinableであり、安全にムーブ代入を使用できます。しかし、f2のt2はjoinableです(コンストラクタでtidが初期化されているため)。ムーブ代入の際にはjoinableかどうかが判断され、joinableであれば直接terminateします。これは実は理解しやすいことです。t2自体のスレッドが作成されたのに、まだ消費・使用されないうちにswapで外に出てしまうからです。では、この問題はどのように修正すればよいのでしょうか?

実はとても簡単です:その前にjoinを1つ加えるだけです:

t2.join();  // 新加的
t2 = std::move(t1);
t2.join();

t2に対して2回joinしているのに問題ないのかと感じる人もいるかもしれません。このとき2回目のt2は実際にはすでにt1なので、次のように見なすことができます:

t2.join();
t1.join();

3.C++20のjthread

C++20以降、std::jthreadが導入されました。これは起動したスレッドを自動的にjoinし、割り込みシグナルを発行することができます。

3.1 基本的な使い方

もし前述のthreadを使った場合、次のコードはcoreします。デストラクタの中でjoinableかどうかが判断され、このときterminateされるためです。

std::thread t{ [] {std::cout << "hello std::thread" << std::endl; } };

std::cout << "t.joinable(): " << t.joinable() << std::endl;

C++20のjthreadを使えば、このような問題は起こりません。例えば:

std::jthread tj{ [] {std::cout << "hello std::jthread" << std::endl; } };

std::cout << "tj.joinable(): " << tj.joinable() << std::endl;

このとき、正常に実行できることが分かります!

3.2 STL実装

jthreadはthreadの中にあり、その実装は前述のthreadに基づいています。ソースコードの場所:gcc/libstdc++-v3/include/std/thread

いったいどのような秘密があるのでしょうか?一緒に解き明かしていきましょう。

3.2.1 threadとの関連

jthreadはthreadと密接に関係しており、threadに基づいて実装されていると言えます。その中で最も核心的なメンバがthread型であり、内部のidとnative_handle_type型はどちらもthread内部のものを使用しています。

using id = thread::id;
using native_handle_type = thread::native_handle_type;

thread _M_thread;

3.2.2 stop_tokenとstop_source

jthreadの内部では、std::stop_tokenを使って制御を強化しています。std::stop_tokenは、スレッドの実行に対して停止要求を送るための機構です。それは要求として動作し、スレッドの実行の中で正しく処理されれば、終了が許可されます。

内部ではstd::stop_sourceメンバを使って管理しています。

stop_source _M_stop_source;

では、一緒にSTLの実装を見ていきましょう。

1.stop_source

まず、stop_sourceの内部はstateを1つ維持しています。その名前は:

stop_token::_Stop_state_ref _M_state;

その中身は次のとおりです:

_Stop_state_t* _M_ptr

1つのstop_sourceは異なるスレッドに保持されうるため、共有可能です。したがって内部は参照によって実装されています。そのため、ソースコードの中で_Stop_state_refを見かけるかもしれません。これがstate全体のライフサイクルと所有権の管理を担当します。共有の実装は下層の_Stop_state_tによって行われており、その内部には重要なメンバが2つあります:

using value_type = uint32_t;
std::atomic<value_type> _M_owners{1};
std::atomic<value_type> _M_value{_S_ssrc_counter_inc};

1つは_M_ownersで、所有権を管理します。_M_valueはいつ停止するかを管理します。

私たちが普段使う3つのインターフェースは、それぞれ次のとおりです:

  • stop_possible
  • stop_requested
  • request_stop

下層では_M_valueに対応するbitが設定されます。

static constexpr value_type _S_stop_requested_bit = 1;
static constexpr value_type _S_locked_bit = 2;
static constexpr value_type _S_ssrc_counter_inc = 4;

ユーザーが停止を要求すると、すなわちrequest_stopを実行すると、_S_stop_requested_bitが設定されます。ユーザーがstop_requestedを呼び出した場合も、内部の_M_valueに_S_stop_requested_bitが設定されているかどうかを確認しに行きます。

_S_locked_bitはstop_possibleインターフェースと関係があります。停止要求がすでに発行されているか、まだ停止要求を発行可能な状態であれば、stop_possibleはtrueを返します。

もう1つは_S_ssrc_counter_incで、これはデフォルト値です。stop_sourceの構築/コピーコンストラクトの際にvalueに_S_ssrc_counter_incが加算されます。

2.stop_token

上で述べた_Stop_state_refの内容は、すべて基本的にstop_tokenの中にあります。その内部もstateを1つ維持しています。その名前は:

_Stop_state_ref _M_state;

実装の詳細は上記と同じなので、繰り返しは省略します。では、stop_sourceとの違いは何でしょうか?

ここではcppreferenceの説明を引用します:stop_tokenクラスは、関連付けられたstd::stop_sourceオブジェクトに対して停止要求が発行されたか、または発行されうるかを確認する方法を提供します。それは本質的に、関連する停止状態に対するスレッドセーフな「ビュー」です。

https://en.cppreference.com/w/cpp/thread/stop_token

3.stop_callback

stop_callbackとstop_tokenの関係は、stop_callbackがstop_tokenに登録されるというものです。stop_tokenの内部では双方向リンクリスト構造を維持してcallbackの削除と追加を行っており、callbackの挿入には先頭挿入法が使われています。

callbackがトリガーされるタイミングは2つあります:

  • request_stop時にトリガーされる

callbackが事前にstop_tokenに登録されていれば、しばらく実行した後でユーザーが要求を発行したときに、callbackがトリガーされます。

auto onStop = []() {
    std::cout << "Timer has stopped." << std::endl;
};

// Register the onStop callback with stop_callback
std::stop_callback callback(source.get_token(), onStop);
// do something for example: sleep

// stop
source.request_stop(); // invoke callback
  • 登録時に即座にトリガーされる

stop_tokenがすでに停止を要求している場合、その時点でcallbackは登録時に即座にトリガーされます。スレッドの実行がすでに終わっているため、このときcallbackを実行すべきだからです。

thread.request_stop();
std::stop_callback callback(stopToken, []() {
  std::cout << "Callback executed" << std::endl;
}); // invoke callback

STLでの具体的な実装は、_M_register_callbackが__cb->_M_run()を呼び出し、このときcallback関数が呼び出されるというものです。

3.2.3 構築と破棄

3.2.3.1 構築
  • デフォルト構築

デフォルト構築では空のstopstateが初期化され、内部のtidは管理しません。動作はthreadと同じです。

jthread() noexcept
  : _M_stop_source{nostopstate}
  { }
  • 引数付き構築

_M_threadを初期化します。これは_S_createを通じて_S_create_pmfを呼び出してthreadを作成し、それを_M_threadに与えることで、内部のthreadが構築されます。

template<typename _Callable, typename... _Args,
   typename = enable_if_t<!is_same_v<remove_cvref_t<_Callable>,
             jthread>>>
  explicit
  jthread(_Callable&& __f, _Args&&... __args)
  : _M_thread{_S_create(_M_stop_source, std::forward<_Callable>(__f),
      std::forward<_Args>(__args)...)}
  { }
3.2.3.2 コピーとムーブ

コピーコンストラクタとコピー代入は無効化されており、動作はthreadと同じです!

jthread(const jthread&) = delete;
jthread& operator=(const jthread&) = delete;

ムーブコンストラクタはデフォルト、ムーブ代入はswapを使用します。

jthread(jthread&&) noexcept = default;

jthread&
operator=(jthread&& __other) noexcept
{
  std::jthread(std::move(__other)).swap(*this);
  return *this;
}

void
swap(jthread& __other) noexcept
{
  std::swap(_M_stop_source, __other._M_stop_source);
  std::swap(_M_thread, __other._M_thread);
}
3.2.3.3 破棄

破棄がjthreadの最初の動作を決定しています。なぜ私たちが手動でjoinしなくてよいのかというと、破棄のときにjoin可能かどうかを判断し、joinableであれば、ユーザーがjoinしていないので、デストラクタが代わりにjoinを呼び出してくれるからです。同時にその前にrequest_stopのチェックも行われます。

~jthread()
{
  if (joinable())
    {
      request_stop();
      join();
    }
}
3.2.3.4 その他のメンバ

その他のメンバは基本的にthreadに対する操作であり、その後インターフェースとして外部に公開しています。例えば:

[[nodiscard]] bool
joinable() const noexcept
{
  return _M_thread.joinable();
}

join()
{
  _M_thread.join();
}

void
detach()
{
  _M_thread.detach();
}

[[nodiscard]] id
get_id() const noexcept
{
  return _M_thread.get_id();
}

[[nodiscard]] native_handle_type
native_handle()
{
  return _M_thread.native_handle();
}

[[nodiscard]] static unsigned
hardware_concurrency() noexcept
{
  return thread::hardware_concurrency();
}

残りはいくつかのstop_sourceに対する操作です:

[[nodiscard]] stop_source
get_stop_source() noexcept
{
  return _M_stop_source;
}

[[nodiscard]] stop_token
get_stop_token() const noexcept
{
  return _M_stop_source.get_token();
}

bool request_stop() noexcept
{
  return _M_stop_source.request_stop();
}

3.3 サンプル

cppreferenceのこの例は非常に良いものです。このサンプルでは、ユーザーの作業関数にstop_sourceが引数として渡されており、その内部でstop_requested()を判断することで、停止された場合にリソースを解放する操作などを行うことができ、非常に役立ちます。

トリガーの条件は、メインスレッドがrequest_stopを実行することです。

https://en.cppreference.com/w/cpp/thread/stop_source

関連トピック

コメント