C++ 基礎からマスターまで — 第20回
スレッド、atomic、メモリモデルで並行処理を扱う
本ブログの記事は、講義を目的として作成しています。 内容に誤りが含まれる可能性があるため、対象コンパイラと標準ライブラリの資料も確認してください。
複数スレッドが同じメモリにアクセスする問題は、実行順序が変わるだけではありません。C++ memory model では、同期されていない data race は undefined behavior です。
共有不変条件を明示的に守る
class Inventory
{
public:
void Add(Item item)
{
std::lock_guard lock(Mutex);
Items.push_back(std::move(item));
}
private:
std::mutex Mutex;
std::vector<Item> Items;
};
std::lock_guard は例外を含め scope の終了時に mutex を解放します。ロックは vector の一操作だけでなく、一緒に守るべき不変条件全体を保護します。
複数 mutex を異なる順番で取得すると deadlock が起こり得ます。必要なら std::scoped_lock で同時にロックするか、プロジェクト全体の取得順序を決めます。
Atomic は小さな独立値に向く
std::atomic<unsigned int> completedJobs{0};
void MarkCompleted()
{
completedJobs.fetch_add(1, std::memory_order_relaxed);
}
他の状態を公開しない統計カウンタなら relaxed ordering で十分な場合があります。ただし一つの atomic は、複数フィールドの整合性を自動で守りません。
既定の sequential consistency は推論しやすい順序を提供します。acquire、release、relaxed は、性能予想ではなく証明された happens-before 関係に基づいて選びます。
jthread で終了を寿命に結び付ける
void Worker(std::stop_token stop)
{
while (!stop.stop_requested())
ProcessOneJob();
}
void Run()
{
std::jthread worker(Worker);
// デストラクタが停止要求と join を行う。
}
C++20 の std::jthread は破棄時に停止を要求し、終了を待ちます。stop_token は協調的キャンセルなので、応答しない blocking I/O には timeout や別の起床手段が必要です。
まず共有変更状態を減らします。worker に独立した入力を与え、message や queue で結果を集めます。別々の atomic でも同じ cache line にあると false sharing が発生し得ます。
一度テストが通っても安全性の証明にはなりません。所有権、終了条件、ロック順序、公開規則を記録し、ThreadSanitizer と再現可能なストレステストで検証します。