1. 導入
C++のスマートポインタであるstd::shared_ptrは、メモリ管理を自動化する非常に強力なツールです。しかし、実務の現場では「shared_ptrを使っているからスレッドセーフだ」という誤った認識が、深刻なデータ競合(Race Condition)を引き起こすことがあります。本記事では、shared_ptrがどこまでを保証し、どこからが開発者の責任なのかを明確に解説します。
2. 基礎知識
std::shared_ptrが持つスレッド安全性には「二つの側面」があります。
参照カウンタの操作:
複数のスレッドから同一のshared_ptrインスタンスをコピーしたり、破棄したりする際の参照カウンタの増減(インクリメント・デクリメント)は、標準ライブラリ側でアトミック操作として保証されています。つまり、カウンタの更新自体でメモリ破壊が起きることはありません。
オブジェクトへのアクセス:
これが最も重要な点ですが、shared_ptrが管理している「中身のオブジェクト(ポインタが指す先)」へのアクセスは、一切保護されていません。複数のスレッドから同時に同じオブジェクトを書き換える場合、別途std::mutexなどを用いた同期処理が必須となります。
3. 実装/解決策
shared_ptr自体は「スマートポインタという器」の安全性を保証するものであり、「器の中身」の安全性を保証するものではありません。
「複数のスレッドで共有するオブジェクト」を安全に扱うための鉄則は以下の通りです。
1. shared_ptrのコピーや代入は、標準の機能によりスレッドセーフである。
2. 同一のshared_ptrオブジェクトを複数のスレッドで同時に読み書きする場合は、std::atomic
3. 中身のオブジェクトへのアクセスは、必ずミューテックスなどで同期をとる。
4. サンプルプログラム
以下に、mutexを用いて中身のデータを保護する実用的なパターンを示します。
include
include
include
include
// 共有するデータ構造
struct SharedData {
int value = 0;
std::mutex mtx; // データを保護するためのミューテックス
void increment() {
std::lock_guard
value++;
}
};
int main() {
// shared_ptrでデータを共有
auto data = std::make_shared
auto worker = [data]() {
for (int i = 0; i < 1000; ++i) {
// カウンタの増減はshared_ptrが管理するためコピーは安全だが、
// 中身のincrement操作はmutexで同期が必要
data->increment();
}
};
// 複数スレッドで実行 現場で陥りやすいバグとして、「shared_ptrそのものの変数自体」を複数のスレッドで書き換えるケースがあります。 C++20以前の環境でshared_ptr変数を複数のスレッドで更新し合う必要がある場合は、必ずstd::mutexでshared_ptrオブジェクト自体を保護してください。C++20以降であれば、std::atomic 「shared_ptrは魔法の杖ではない」という意識を持ち、常に「ポインタの更新」と「データの実体へのアクセス」を分けて考えることが、バグのないマルチスレッドプログラミングの第一歩です。
std::vector
for (int i = 0; i < 10; ++i) threads.emplace_back(worker);
for (auto& t : threads) t.join();
std::cout << "最終的な値: " << data->value << std::endl; // 10000になれば成功
return 0;
}
5. 応用・注意点
例えば、スレッドAがshared_ptrを上書きし、スレッドBが同時にそのshared_ptrを参照しようとすると、参照カウンタの操作以前に「shared_ptrオブジェクト自体のデータ競合」が発生します。

コメント