Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】C++ 20 multi threaded programming - the introduction of std::atomic...

2025年09月19日に「Dev.to」が公開したITニュース「C++ 20 multi threaded programming - the introduction of std::atomic...」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

マルチスレッド処理で複数のスレッドが同じデータに同期なく同時にアクセスすると、「データ競合」が発生し、プログラムの出力が不安定になる。C++20で導入された`std::atomic`を使えば、変数をスレッドセーフに操作できるため、データ競合を防ぎ、並行処理を安全かつ正確に実行できる。

ITニュース解説

システムエンジニアを目指す初心者がプログラム開発を学ぶ上で、特に最近のコンピューターが持つ複数の処理能力を最大限に活用する「マルチスレッドプログラミング」は避けて通れない重要なテーマだ。マルチスレッドとは、一つのプログラム内で複数の処理の流れ(スレッド)を同時に実行することを指す。これにより、計算量の多い処理を効率良く進めたり、ユーザーインターフェースが固まることなくバックグラウンドで処理を行ったりすることが可能になる。しかし、複数のスレッドが同じデータにアクセスしようとするときに、予期せぬ問題が発生することがある。それが「データ競合(Data Race)」と呼ばれる現象だ。

データ競合とは、複数のスレッドが共通のデータに同時にアクセスし、少なくとも一つのアクセスがデータを書き換えるものであるにもかかわらず、そのアクセス順序が同期メカニズムによって制御されていない場合に起こる現象をいう。これは、たとえるなら、複数の人が同時に一つのホワイトボードに書き込みをしようとする状況に似ている。誰がどのタイミングで書き、誰が読み取るかというルールがないため、最終的に何が書かれているか分からなくなったり、書きかけの文字が消されたりする可能性がある。

具体的にデータ競合が発生する原因はいくつか考えられる。一つは「同時読み取りと書き込み」だ。あるスレッドが変数の値を読み込んでいる最中に、別のスレッドがその変数の値を書き換えてしまうと、読み取った値が最新のものでなかったり、途中で変わってしまったりする可能性がある。二つ目は「同時書き込み」だ。複数のスレッドが同時に同じ変数に値を書き込もうとすると、どのスレッドの書き込みが最終的に反映されるか保証されず、結果が予測できなくなる。そして、これらの問題を引き起こす根本的な原因は、「同期プリミティブの欠如」にある。同期プリミティブとは、mutex(ミューテックス)やアトミック操作など、スレッド間のデータの安全な共有を保証するための仕組みのことだ。これらを使用しないと、データ競合が頻繁に発生し、プログラムの出力が予測不能になったり、最悪の場合クラッシュしたりする事態に発展する。

実際のプログラムの例でこの問題を考えてみよう。例えば、counterという一つの整数変数があり、二つのスレッドがそれぞれcounterの値を10000回インクリメント(1ずつ増やす)する処理を行うとする。期待される最終的なcounterの値は、当然20000になるはずだ。しかし、同期メカニズムを使わない場合、この期待は裏切られることが多い。

なぜ、このようなことが起こるのだろうか。counter++という一見単純に見える操作も、コンピューター内部ではいくつかのステップに分解される。具体的には、

  1. 現在のcounterの値をメモリから読み込む。
  2. 読み込んだ値に1を足す。
  3. 新しい値をメモリのcounterに書き込む。 この三つのステップだ。

もし、スレッドAがcounterの値を読み込み(例えば0)、それに1を足し(1になる)、まだメモリに書き込む前の段階で、スレッドBもcounterの値を読み込んでしまったらどうなるだろうか。スレッドBも古い値(0)を読み取り、それに1を足し(1になる)、そしてメモリに書き込む。その後にスレッドAが自分の計算結果(1)をメモリに書き込む。この一連の動きの結果、二つのスレッドがそれぞれ1ずつ増やしたにもかかわらず、counterの値は最終的に1になってしまう。このように、複数のスレッドが同時にアクセスすることで、それぞれの操作が途中で中断され、結果的に期待通りの値にならない現象がデータ競合の典型的な例だ。この例では、counterが10000回ずつ増える処理が繰り返されるため、毎回このような競合が発生する可能性があり、最終的な値は20000よりもはるかに少ない、不正確なものになる。

このようなデータ競合の問題を解決するために、C++にはさまざまな同期メカニズムが用意されている。その中でも、C++20から特に注目されているのが「std::atomic」という機能だ。std::atomicは、特定のデータ型をアトミック(不可分)なものとして扱うことを可能にする。アトミックとは、そのデータに対する操作(読み取り、書き込み、変更など)が、他のスレッドから見て途中で割り込まれることなく、一つのまとまった処理として完了することを意味する。つまり、先ほどのcounter++の例でいえば、std::atomic<int>型の変数に対して++演算子を使用すると、読み込み、変更、書き込みの三つのステップが、他のスレッドからは割り込めない「一つの操作」として扱われるようになる。

std::atomicを使って先ほどのプログラムを修正すると、コードは次のように変わる。

1#include <atomic>
2// int counter = 0; の代わりに
3std::atomic<int> counter{0}; // std::atomic<int>型を使用

このようにint型だったcounter変数をstd::atomic<int>型に変更するだけで、incrementCounter関数内のcounter++という操作は自動的にアトミックな操作になる。これにより、たとえ複数のスレッドが同時にcounter++を実行しようとしても、それぞれのスレッドは他のスレッドの邪魔をすることなく、安全にcounterの値を増やすことができる。

最終的なcounterの値を表示する際にも少し違いがある。std::atomic型の変数の値を取得する際には、counter.load()のように.load()メソッドを使うのが一般的だ。これは、アトミックな読み込み操作を明示的に行うことを意味する。

std::atomicの導入により、先ほどのデータ競合が起きていたプログラムは、正確に20000という結果を出力するようになる。これは、std::atomicが内部的に、CPUの特殊な命令(アトミック命令)や、必要に応じて軽いロック機構(スピンロックなど)を活用することで、データの安全な同時アクセスを保証しているためだ。プログラマは、複雑なロックメカニズムを自分で実装することなく、変数をstd::atomic型にするだけで簡単にスレッドセーフなコードを書けるようになる。

C++は、長年にわたり進化を続けているプログラミング言語であり、C++20でのstd::atomicの強化のように、マルチスレッドプログラミングにおける安全で効率的な開発をサポートするための機能が着実に導入されている。システムエンジニアを目指す上で、このような並行処理の概念とその解決策であるstd::atomicのようなツールの理解は、高性能で信頼性の高いアプリケーションを開発するために不可欠な知識となるだろう。安全なマルチスレッドプログラミングは、今日のコンピューティング環境において、ソフトウェアのパフォーマンスと安定性を確保するための鍵と言える。

関連コンテンツ