C++ マルチスレッドの排他処理(クリティカルセクション)

前書き

C++のマルチスレッド処理を過去の資産を参考にして書いていたが、だんだんと自分の理解が怪しくなってきたので、いまさらながらC++11のスレッド処理を調査した。特に排他処理関係の備忘を載せておく。

マルチスレッド排他処理の基本

排他処理の基本はMutexを使う。Windows的にはクリティカルセクションとミューテックス

は別物で、使用目的や速度を考慮して使い分けるものである。しかし、C++11では基本的にクリティカルセクションの実現に std::mutex というものを使用する。どうやら内部的にはWinAPIのMutexではなく、別の同期処理を呼び出しているらしい。

あるスレッドが std::mutex インスタンスのlock関数を呼び出すと、同じスレッドでunlock関数を呼び出すまで、他のスレッドは同一インスタンスのlock関数を呼び出すことができなくなる。ただし、いちいちlock関数やunlock関数を呼ぶようなコードを書くと、途中でreturnなどが来た場合にunlockし忘れる可能性がある。

なので、基本的にはスコープドロックの仕組みを使って、以下のように書く。


std::mutex mtx;

void ClassA::funcA()
{
    // 非同期処理

    {
        std::lock_guard lock(mtx);

        // クリティカルセクション

    }

    // 非同期処理
}

これだけで、std::lock_guardクラスのコンストラクタとデストラクタが自動的にlock・unlockを呼び出してくれる。


注意

先ほどのコードは参考用なので簡略化しているが、ここでひとつ気を付けなければいけないことがある。mtx自体のスコープである。ClassAのメンバーなら、所属するClassAインスタンス内だけで排他処理を行い、インスタンス間では排他にならない。


class ClassA {
private:
    std::mutex mtx;

public:
    void funcA()
    {
        std::lock_guard lock(mtx);

        // クリティカルセクション
    }
};

int main()
{
    ClassA ca;

    // スレッドを4つ作る
    std::vector ths(4);
    for (auto& th : ths) {
        th = std::thread([&ca] { ca.funcA(); });
    }

    // すべてのスレッドが終わるのを待つ
    for (auto& th : ths) {
        th.join();
    }

    return 0;
}

mtxがClassAのメンバーではなく、グローバルもしくはスタティックな変数であれば、全てのClassAインスタンス間で排他が行われる。

特にC++の場合、クラスを使わない関数プログラムが記述可能で、以下のような関数ベースのサンプルコードからクラスに適用する場合に、どういう目的で排他をするのか、気を付けなければいけない。


std::mutex mtx;

void funcA() {
    std::lock_guard lock(mtx);

    // クリティカルセクション
}

int main() {

    // スレッドを4つ作る
    std::vector ths(4);
    for (auto& th : ths) {
        th = std::thread(funcA);
    }

    // すべてのスレッドが終わるのを待つ
    for (auto& th : ths) {
        th.join();
    }

    return 0;
}