【C++学習|豆知識】C++20の新常識!std::atomic>でスレッドセーフな共有を実現しよう

導入

マルチスレッドプログラミングにおいて、共有データへの安全なアクセスは最も頭を悩ませる課題の一つです。特にスマートポインタであるstd::shared_ptrは、ポインタのコピーや破棄を管理する「制御ブロック」を内部で持っていますが、ポインタ変数自体の書き換えはスレッドセーフではありません。C++20以前は、std::shared_ptrをスレッド間で安全に共有するためにstd::mutexを用いる必要があり、これがパフォーマンスのボトルネックになることもありました。C++20で導入されたstd::atomic>は、このオーバーヘッドを最小限に抑え、スマートポインタの更新をロックフリー(または効率的なロック)で実現するための強力な解決策です。

基礎知識

まず前提として、std::shared_ptrは「参照カウント」を管理することで、オブジェクトの生存期間を自動制御します。しかし、std::shared_ptrのオブジェクトそのもの(ポインタが指す先を入れ替える操作など)は、デフォルトではアトミックではありません。複数のスレッドから同時に同じshared_ptr変数にアクセスして代入操作を行うと、最悪の場合データ競合が発生し、プログラムがクラッシュします。std::atomicは、ある変数への操作が「一瞬で完了するように見える」ことを保証するテンプレートクラスであり、これにstd::shared_ptrを組み合わせることで、ポインタの付け替えを安全に行えるようになります。

実装/解決策

std::atomic>を使用する場合、通常のstd::atomicなどとは異なり、メンバ関数としてload()やstore()を使用します。これらは内部的に参照カウントの操作を適切に行うため、開発者はmutexを意識することなく、安全にインスタンスを共有できます。注意点として、この機能を使うには対応したコンパイラでC++20規格を有効にする必要があります。

サンプルプログラム

以下のコードは、複数のスレッドから安全にshared_ptrを更新する例です。

include
include
include
include include

int main() {
// スレッドセーフなshared_ptrのアトミック管理
std::atomic> atomic_ptr;

// 初期化
atomic_ptr.store(std::make_shared(0));

auto worker = [&atomic_ptr](int id) {
// 現在のポインタを取得して新しい値で更新
std::shared_ptr old_ptr = atomic_ptr.load();
auto new_ptr = std::make_shared(id);

// compare_exchange_strongを使ってスレッドセーフに更新を試みる
// 成功するまでループして更新を確定させる
while (!atomic_ptr.compare_exchange_weak(old_ptr, new_ptr)) {
// 失敗した場合はold_ptrが最新の状態に更新されるため再試行
}
std::cout << "スレッド " << id << " が値を更新しました" << std::endl; }; std::vector threads;
for (int i = 1; i <= 5; ++i) { threads.emplace_back(worker, i); } for (auto& t : threads) { t.join(); } std::cout << "最終値: " << atomic_ptr.load() << std::endl; return 0; }

応用・注意点

この機能は非常に便利ですが、注意すべき点がいくつかあります。まず、std::atomic>は、実装によっては内部でロックを使用する場合があるという点です。これは、プラットフォームやハードウェアの制限により、巨大なスマートポインタのコピーをアトミックに実行できない場合に発生します。また、compare_exchange_weakなどの操作は、誤った使い方をすると無限ループに陥る可能性があるため、ループ処理の記述には十分注意してください。現場では、頻繁にポインタを書き換えるような設計ではなく、一度構築したオブジェクトを読み取り専用で共有する設計を目指すのが、パフォーマンスを最大化するコツです。

コメント

タイトルとURLをコピーしました