第7章:メモリモデルと順序付け

この章で学ぶこと

  • C11のメモリモデル
  • アトミック操作
  • メモリオーダー
  • happens-before関係
  • データ競合と未定義動作
  • ---

    7.1 なぜメモリモデルが必要か

    並行プログラミングの問題

    マルチスレッドプログラムでは、複数のスレッドが同時にメモリにアクセスします。

    int x = 0;
    int y = 0;
    
    /* スレッド1 */
    x = 1;
    y = 1;
    
    /* スレッド2 */
    int r1 = y;
    int r2 = x;
    

    直感的には r1 == 1 && r2 == 0 は不可能に見えますが、実際には起こりえます。

    原因

  • コンパイラの最適化(リオーダー)
  • CPUの実行順序最適化
  • キャッシュの影響
  • ---

    7.2 アトミック型と操作

    _Atomic 修飾子

    #include <stdatomic.h>
    
    _Atomic int counter = 0;
    atomic_int counter2 = ATOMIC_VAR_INIT(0);
    

    アトミック操作

    atomic_store(&counter, 42);
    int value = atomic_load(&counter);
    atomic_fetch_add(&counter, 1);
    

    複合代入

    counter++;  /* アトミック */
    counter += 5;  /* アトミック */
    

    ---

    7.3 メモリオーダー

    6種類のメモリオーダー

    typedef enum memory_order {
        memory_order_relaxed,
        memory_order_consume,
        memory_order_acquire,
        memory_order_release,
        memory_order_acq_rel,
        memory_order_seq_cst
    } memory_order;
    

    各オーダーの意味

    relaxed: 順序保証なし。アトミック性のみ。

    acquire: このロード以降の読み書きが、このロード前にリオーダーされない。

    release: このストア以前の読み書きが、このストア後にリオーダーされない。

    acq_rel: acquire と release の両方。

    seq_cst: 逐次一貫性。最も厳しい。デフォルト。

    使用例

    /* プロデューサー・コンシューマー */
    atomic_store_explicit(&data_ready, 1, memory_order_release);
    
    while (atomic_load_explicit(&data_ready, memory_order_acquire) == 0)
        ;
    /* data にアクセス */
    

    ---

    7.4 happens-before関係

    定義

    操作Aが操作Bより先に「happens-before」する場合、Aの効果はBから見える。

    成立条件

  • 同一スレッド内での順序
  • release操作とそれを観測するacquire操作
  • /* スレッド1 */
    data = 42;  /* A */
    atomic_store_explicit(&flag, 1, memory_order_release);  /* B */
    
    /* スレッド2 */
    while (atomic_load_explicit(&flag, memory_order_acquire) == 0)  /* C */
        ;
    int local = data;  /* D */
    

    B happens-before C、したがって A happens-before D。local は 42。

    ---

    7.5 データ競合

    定義

    規格 5.1.2.4p25:

    > Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.

    データ競合は、競合するアクセスがあり、happens-before関係がない場合に発生します。

    データ競合は未定義動作

    int x = 0;
    
    /* スレッド1 */
    x = 1;
    
    /* スレッド2 */
    int y = x;
    
    /* データ競合 → 未定義動作 */
    

    解決方法

    _Atomic int x = 0;
    
    /* スレッド1 */
    atomic_store(&x, 1);
    
    /* スレッド2 */
    int y = atomic_load(&x);
    
    /* OK: アトミック操作 */
    

    ---

    7.6 ミューテックスと条件変数

    C11スレッド(オプション)

    #include <threads.h>
    
    mtx_t mutex;
    mtx_init(&mutex, mtx_plain);
    
    mtx_lock(&mutex);
    /* クリティカルセクション */
    mtx_unlock(&mutex);
    

    POSIXスレッド

    #include <pthread.h>
    
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    pthread_mutex_lock(&mutex);
    /* クリティカルセクション */
    pthread_mutex_unlock(&mutex);
    

    ---

    7.7 この章のまとめ

    C11メモリモデル

  • アトミック型と操作
  • 6種類のメモリオーダー
  • happens-before関係
  • データ競合

  • 競合 + happens-before関係なし = 未定義動作
  • アトミック操作または同期プリミティブで解決

---

確認問題

問題1

データ競合が未定義動作である理由は?

解答

規格がデータ競合を未定義動作と定義しているため。最適化や実行順序の変更に自由度を与えるため。

問題2

memory_order_seq_cstmemory_order_relaxed の違いは?

解答

  • seq_cst: 逐次一貫性。すべてのスレッドが同じ順序で操作を観測。最も厳しい。
  • relaxed: アトミック性のみ保証。順序の保証なし。最も緩い。

---

次の章では、標準ライブラリの仕様詳細について学びます。