web-dev-qa-db-ja.com

<atomic>を使用したSpinlockのC ++ 11実装

次のようにSpinLockクラスを実装しました

struct Node {
    int number;
    std::atomic_bool latch;

    void add() {
        lock();
        number++;
        unlock();
    }
    void lock() {
        bool unlatched = false;
        while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
    }
    void unlock() {
        latch.store(false , std::memory_order_release);
    }
};

上記のクラスを実装し、Node classの同じインスタンスのadd()メソッドをスレッドごとに1,000万回呼び出す2つのスレッドを作成しました。

結果は、残念ながら2000万ではありません。ここで何が欠けていますか?

31
syko

問題は、compare_exchange_weakが失敗するとunlatched変数を更新することです。 compare_exchange_weakのドキュメントから:

アトミックオブジェクトに含まれる値の内容を期待値と比較します。-trueの場合、含まれている値をval(storeなど)に置き換えます。 -falseの場合、期待値を含まれる値に置き換えます。

すなわち、最初に失敗したcompare_exchange_weakの後、unlatchedtrueに更新されるため、次のループの繰り返しはcompare_exchange_weaktruetrue。これは成功し、別のスレッドが保持しているロックを取得しました。

解決策:各compare_exchange_weakの前に、必ずunlatchedfalseに戻すようにしてください。例:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}
40
gexicide

@gexicideで述べたように、問題は_compare_exchange_関数がアトミック変数の現在の値でexpected変数を更新することです。それが理由であり、そもそもローカル変数unlatchedを使用しなければならない理由です。これを解決するために、各ループの繰り返しでunlatchedをfalseに戻すことができます。

ただし、インターフェイスがあまり適していないものに_compare_exchange_を使用する代わりに、代わりに_std::atomic_flag_を使用する方がはるかに簡単です。

_class SpinLock {
    std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { ; }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};
_

ソース: cppreference

手動でメモリの順序を指定することは、ソースからコピーした潜在的なパフォーマンスの微調整にすぎません。パフォーマンスの最後の部分よりも単純さが重要な場合は、デフォルト値のままにしてlocked.test_and_set() / locked.clear()を呼び出すことができます。

ところで:_std::atomic_flag_は、ロックフリーであることが保証されている唯一のタイプですが、_std::atomic_bool_の操作がロックフリーではないプラットフォームは知りません。

更新:@David Schwartz、@ Anton、および@Technik Empireのコメントで説明されているように、空のループには、分岐ミス予測、スレッド枯渇などの望ましくない効果がありますHTプロセッサと非常に高い電力消費で-つまり、待機するのはかなり非効率的な方法です。影響とソリューションは、アーキテクチャ、プラットフォーム、およびアプリケーション固有です。私は専門家ではありませんが、通常の解決策は、Linuxのcpu_relax()またはwindowsのYieldProcessor()をループ本体に追加することです。

EDIT2:明確にするために:ここに示されている移植可能なバージョン(特別なcpu_relaxなどの指示なし)は、多くのアプリケーションで十分であるはずです。他の誰かが長時間ロックを保持しているためにSpinLockが頻繁に回転する場合(これは、一般的な設計上の問題を既に示している可能性があります)、とにかく通常のミューテックスを使用する方がよいでしょう。

33
MikeMB