C ++ 0xにはセマフォがありませんか?スレッドを同期するには?


135

C ++ 0xがセマフォなしで提供されるのは本当ですか?セマフォの使用に関するスタックオーバーフローに関する質問はすでにいくつかあります。私はそれら(posixセマフォ)を常に使用して、スレッドに別のスレッドのイベントを待機させます。

void thread0(...)
{
  doSomething0();

  event1.wait();

  ...
}

void thread1(...)
{
  doSomething1();

  event1.post();

  ...
}

私がミューテックスでそれをするなら:

void thread0(...)
{
  doSomething0();

  event1.lock(); event1.unlock();

  ...
}

void thread1(...)
{
  event1.lock();

  doSomethingth1();

  event1.unlock();

  ...
}

問題:醜く、スレッド1が最初にミューテックスをロックすることは保証されていません(同じスレッドがミューテックスをロックおよびロック解除する場合、スレッド1およびスレッド1が開始する前にイベント1をロックすることもできません)。

だからブーストにもセマフォがないので、上記を達成する最も簡単な方法は何ですか?


多分、条件mutexとstd :: promiseとstd :: futureを使用しますか?
Yves

回答:


179

ミューテックスと条件変数から簡単に作成できます。

#include <mutex>
#include <condition_variable>

class semaphore
{
private:
    std::mutex mutex_;
    std::condition_variable condition_;
    unsigned long count_ = 0; // Initialized as locked.

public:
    void notify() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        ++count_;
        condition_.notify_one();
    }

    void wait() {
        std::unique_lock<decltype(mutex_)> lock(mutex_);
        while(!count_) // Handle spurious wake-ups.
            condition_.wait(lock);
        --count_;
    }

    bool try_wait() {
        std::lock_guard<decltype(mutex_)> lock(mutex_);
        if(count_) {
            --count_;
            return true;
        }
        return false;
    }
};

96
誰かが標準委員会に提案を提出する必要があります

7
ここで私が最初に戸惑ったコメントは、待機中のロックです。ロックが待機によって保持されている場合、スレッドは通知をどのように通過できるのでしょうか。ややあいまいに文書化された答えは、condition_variable.waitがロックをパルスし、別のスレッドがアトミックな方法で通知を通過できるようにすることです。少なくとも、私はそれを理解しています

31
これは、セマフォはプログラマーが自分たちとぶらぶらするにはロープが多すぎるため、意図的に Boostから除外されました。おそらく条件変数の方が扱いやすいでしょう。私は彼らの要点を見ていますが、少しひいきにされています。同じロジックがC ++ 11にも当てはまると思います-プログラマーはcondvarsまたは他の承認された同期技術を「自然に」使用する方法でプログラムを書くことが期待されています。セマフォを指定すると、それがcondvarの上に実装されているかネイティブに実装されているかに関係なく、それに対して実行されます。
スティーブジェソップ

5
注- ループの背後にある根拠については、en.wikipedia.org / wiki / Spurious_wakeupを参照してくださいwhile(!count_)
Dan Nissenbaum、2012年

3
@Maximごめんなさい、あなたが正しいとは思いません。sem_waitとsem_postは競合の場合もsyscallのみです(sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.cを確認してください)。このコードは、潜在的なバグを含むlibc実装を複製することになります。任意のシステムでの移植性を意図している場合はそれが解決策になる可能性がありますが、Posix互換性のみが必要な場合は、Posixセマフォを使用します。
xryl669 14

107

Maxim Yegorushkinの回答に基づいて、C ++ 11スタイルで例を作成しようとしました。

#include <mutex>
#include <condition_variable>

class Semaphore {
public:
    Semaphore (int count_ = 0)
        : count(count_) {}

    inline void notify()
    {
        std::unique_lock<std::mutex> lock(mtx);
        count++;
        cv.notify_one();
    }

    inline void wait()
    {
        std::unique_lock<std::mutex> lock(mtx);

        while(count == 0){
            cv.wait(lock);
        }
        count--;
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

34
:あなたはまた、3-ライナー)(待機を行うことができますcv.wait(lck, [this]() { return count > 0; });
ドミ

2
lock_guardの精神で別のクラスを追加することも役に立ちます。RAII方式では、セマフォを参照として使用するコンストラクタがセマフォのwait()呼び出しを呼び出し、デストラクタがそのnotify()呼び出しを呼び出します。これにより、例外がセマフォの解放に失敗するのを防ぎます。
Jim Hunziker、2014年

デッドロックはありませんか?たとえば、N個のスレッドがwait()およびcount == 0を呼び出した場合、cv.notify_one(); mtxがリリースされていないため、呼び出されることはありませんか?
Marcello

1
@Marcello待機中のスレッドはロックを保持しません。条件変数の要点は、アトミックな「ロック解除および待機」操作を提供することです。
David Schwartz

3
ウェイクアップをすぐにブロックしないように、notify_one()を呼び出す前にロックを解放する必要があります...ここを参照してください:en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

私はできる限り最も堅牢で汎用的なC ++ 11セマフォを標準のスタイルでできる限り記述することにしました(注using semaphore = ...、通常はnot semaphoreを通常使用するのと同じような名前を使用します)。stringbasic_string

template <typename Mutex, typename CondVar>
class basic_semaphore {
public:
    using native_handle_type = typename CondVar::native_handle_type;

    explicit basic_semaphore(size_t count = 0);
    basic_semaphore(const basic_semaphore&) = delete;
    basic_semaphore(basic_semaphore&&) = delete;
    basic_semaphore& operator=(const basic_semaphore&) = delete;
    basic_semaphore& operator=(basic_semaphore&&) = delete;

    void notify();
    void wait();
    bool try_wait();
    template<class Rep, class Period>
    bool wait_for(const std::chrono::duration<Rep, Period>& d);
    template<class Clock, class Duration>
    bool wait_until(const std::chrono::time_point<Clock, Duration>& t);

    native_handle_type native_handle();

private:
    Mutex   mMutex;
    CondVar mCv;
    size_t  mCount;
};

using semaphore = basic_semaphore<std::mutex, std::condition_variable>;

template <typename Mutex, typename CondVar>
basic_semaphore<Mutex, CondVar>::basic_semaphore(size_t count)
    : mCount{count}
{}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::notify() {
    std::lock_guard<Mutex> lock{mMutex};
    ++mCount;
    mCv.notify_one();
}

template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::wait() {
    std::unique_lock<Mutex> lock{mMutex};
    mCv.wait(lock, [&]{ return mCount > 0; });
    --mCount;
}

template <typename Mutex, typename CondVar>
bool basic_semaphore<Mutex, CondVar>::try_wait() {
    std::lock_guard<Mutex> lock{mMutex};

    if (mCount > 0) {
        --mCount;
        return true;
    }

    return false;
}

template <typename Mutex, typename CondVar>
template<class Rep, class Period>
bool basic_semaphore<Mutex, CondVar>::wait_for(const std::chrono::duration<Rep, Period>& d) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_for(lock, d, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
template<class Clock, class Duration>
bool basic_semaphore<Mutex, CondVar>::wait_until(const std::chrono::time_point<Clock, Duration>& t) {
    std::unique_lock<Mutex> lock{mMutex};
    auto finished = mCv.wait_until(lock, t, [&]{ return mCount > 0; });

    if (finished)
        --mCount;

    return finished;
}

template <typename Mutex, typename CondVar>
typename basic_semaphore<Mutex, CondVar>::native_handle_type basic_semaphore<Mutex, CondVar>::native_handle() {
    return mCv.native_handle();
}

これは、少し編集して機能します。述語を持つメソッド呼び出しは、ブール値(ない`のstd :: cv_status)を返します。wait_forwait_until
jdknight 2015年

ゲームの後半が遅くなったため、申し訳ありません。std::size_tは符号なしなので、0未満にデクリメントするとUBになり、常にになります>= 0。私見countはである必要がありintます。
Richard Hodges

3
@RichardHodgesゼロ未満にデクリメントする方法はないので問題はなく、セマフォの負の数は何を意味しますか?それは意味のあるIMOでさえありません。
David

1
@David他の人が物事を初期化するのをスレッドが待たなければならない場合はどうでしょうか?たとえば、1つのリーダースレッドが4つのスレッドを待機する場合、-3を指定してセマフォコンストラクターを呼び出し、他のすべてのスレッドが投稿するまでリーダースレッドを待機させます。それには他の方法があると思いますが、それは合理的ではありませんか?それは実際にはOPが尋ねている質問だと思いますが、より多くの「thread1」が必要です。
jmmut

2
@RichardHodgesは非常に奇妙で、0未満の符号なし整数型をデクリメントすることはUBではありません。
jcai 2017年

15

posixセマフォに従って、私は追加します

class semaphore
{
    ...
    bool trywait()
    {
        boost::mutex::scoped_lock lock(mutex_);
        if(count_)
        {
            --count_;
            return true;
        }
        else
        {
            return false;
        }
    }
};

さらに、より基本的な演算子を使用して、つなぎ合わせたバージョンを常にコピーして貼り付けるのではなく、同期メカニズムを抽象化の便利なレベルで使用することを強く望んでいます。


9

cpp11-on-multicoreもチェックしてください。ます。これには、移植可能で最適なセマフォの実装があります。

リポジトリには、c ++ 11スレッドを補完する他のスレッド機能も含まれています。


8

mutexおよび条件変数を使用できます。mutexを使用して排他的アクセスを取得し、続行するか、もう一方の端を待つ必要があるかを確認します。あなたが待つ必要があるなら、あなたはある状態で待ちます。他のスレッドが続行できると判断すると、条件を通知します。

boost :: threadライブラリに短いがあり、これはおそらくコピーするだけです(C ++ 0xとboostスレッドライブラリは非常に似ています)。


条件は待機中のスレッドのみにシグナルを送るか、そうでないか?では、thread1がシグナルを送ったときにthread0が待機していない場合、後でブロックされますか?さらに、条件に付属する追加のロックは必要ありません-オーバーヘッドです。
タウラン

はい、条件は待機中のスレッドに信号を送るだけです。一般的なパターンは、待機する必要がある場合に備えて、状態と条件を変数に持つことです。プロデューサー/コンシューマーについて考えてみてください。バッファー内のアイテムにカウントがあり、プロデューサーがロックし、要素を追加し、カウントとシグナルをインクリメントします。コンシューマはロックし、カウンタをチェックし、ゼロ以外が消費するかどうか、ゼロの場合は条件で待機します。
DavidRodríguez-

2
この方法でセマフォをシミュレートできます。セマフォに指定する値で変数を初期化し、wait()「ロック、ゼロ以外のデクリメントの場合はカウントを確認して続行します。条件がゼロの場合は待機」postが「ロック、インクリメントカウンター、0の場合はシグナル
デビッドロドリゲス-11

はい、いいですね。posixセマフォも同じように実装されているのでしょうか。
タウラン

@tauran:確かにわかりません(そしてそれはどのPosix OSに依存するかもしれません)が、私はそうは思わないでしょう。セマフォは従来、ミューテックスや条件変数よりも「低レベル」の同期プリミティブであり、原則として、condvarの上に実装した場合よりも効率的にすることができます。そのため、特定のOSでは、すべてのユーザーレベルの同期プリミティブが、スケジューラと対話するいくつかの一般的なツールの上に構築されている可能性が高くなります。
スティーブジェソップ

3

スレッドで有用なRAIIセマフォラッパーにもなります。

class ScopedSemaphore
{
public:
    explicit ScopedSemaphore(Semaphore& sem) : m_Semaphore(sem) { m_Semaphore.Wait(); }
    ScopedSemaphore(const ScopedSemaphore&) = delete;
    ~ScopedSemaphore() { m_Semaphore.Notify(); }

   ScopedSemaphore& operator=(const ScopedSemaphore&) = delete;

private:
    Semaphore& m_Semaphore;
};

マルチスレッドアプリでの使用例:

boost::ptr_vector<std::thread> threads;
Semaphore semaphore;

for (...)
{
    ...
    auto t = new std::thread([..., &semaphore]
    {
        ScopedSemaphore scopedSemaphore(semaphore);
        ...
    }
    );
    threads.push_back(t);
}

for (auto& t : threads)
    t.join();

3

C ++ 20には最終的にセマフォ-がありstd::counting_semaphore<max_count>ます。

これらには(少なくとも)次のメソッドがあります。

  • acquire() (ブロッキング)
  • try_acquire() (非ブロッキング、即時を返します)
  • try_acquire_for() (非ブロッキング、期間がかかります)
  • try_acquire_until() (非ブロッキング、試行を停止するのに時間がかかります)
  • release()

これはまだcppreferenceにリストされていませんが、これらのCppCon 2019プレゼンテーションスライドを読むか、ビデオを見ることができます。公式提案P0514R4もありますが、それが最新バージョンかどうかはわかりません。


2

私は、Shared_ptrとweak_ptr(リスト付きの長い)が必要な仕事をしてくれていることを発見しました。私の問題は、ホストの内部データとやり取りしたいクライアントがいくつかあったことです。通常、ホストは自身でデータを更新しますが、クライアントが要求した場合、クライアントがホストデータにアクセスしなくなるまで、ホストは更新を停止する必要があります。同時に、他のクライアントもホストもそのホストデータを変更できないように、クライアントは排他的アクセスを要求できます。

これを行う方法は、私は構造体を作成しました:

struct UpdateLock
{
    typedef std::shared_ptr< UpdateLock > ptr;
};

各クライアントにはそのようなメンバーがいます。

UpdateLock::ptr m_myLock;

次に、ホストには排他性のためのweak_ptrメンバーがあり、非排他的ロックのためのweak_ptrsのリストがあります。

std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;

ロックを有効にする関数と、ホストがロックされているかどうかを確認する関数があります。

UpdateLock::ptr LockUpdate( bool exclusive );       
bool IsUpdateLocked( bool exclusive ) const;

LockUpdate、IsUpdateLocked、および定期的にホストのUpdateルーチンでロックをテストします。ロックのテストは、weak_ptrの有効期限が切れているかどうかを確認し、m_locksリストから期限切れを削除する(ホストの更新中にのみこれを行う)のと同じくらい簡単で、リストが空かどうかを確認できます。同時に、クライアントがハングしているshared_ptrをリセットすると、自動的にロック解除されます。これは、クライアントが自動的に破棄されたときにも発生します。

全体としての効果は、クライアントが排他性を必要とすることはほとんどないため(通常、追加と削除のためにのみ予約されています)、ほとんどの場合、LockUpdate(false)へのリクエスト、つまり非排他的リクエストは、(!m_exclusiveLock)である限り成功します。また、排他性の要求であるLockUpdate(true)は、(!m_exclusiveLock)と(m_locks.empty())の両方の場合にのみ成功します。

排他的ロックと非排他的ロックを緩和するためにキューを追加することもできますが、これまでのところ衝突はなかったので、解決策が追加されるまで待機するつもりです(ほとんどの場合、実際のテスト条件があります)。

これまでのところ、これは私のニーズにうまく機能しています。これを拡張する必要性、および拡張使用で発生する可能性があるいくつかの問題を想像できますが、これは実装が迅速であり、カスタムコードはほとんど必要ありませんでした。


-4

誰かがアトミックバージョンに興味がある場合、ここに実装があります。パフォーマンスは、ミューテックスと条件変数のバージョンよりも優れています。

class semaphore_atomic
{
public:
    void notify() {
        count_.fetch_add(1, std::memory_order_release);
    }

    void wait() {
        while (true) {
            int count = count_.load(std::memory_order_relaxed);
            if (count > 0) {
                if (count_.compare_exchange_weak(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                    break;
                }
            }
        }
    }

    bool try_wait() {
        int count = count_.load(std::memory_order_relaxed);
        if (count > 0) {
            if (count_.compare_exchange_strong(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
                return true;
            }
        }
        return false;
    }
private:
    std::atomic_int count_{0};
};

4
パフォーマンスはもっと悪くなると思います。このコードは、ほとんどすべての起こり得る間違いを文字通りにしてしまいます。最も明白な例として、waitコードが数回ループする必要があるとします。最終的にブロックが解除されると、CPUのループ予測は確実に再びループすることを予測するため、すべての誤って予測された分岐の母になります。このコードでさらに多くの問題をリストできます。
デビッドシュワルツ

1
次に、もう1つの明らかなパフォーマンスキラーを示します。waitループが回転すると、CPUマイクロ実行リソースが消費されます。notifyそれが想定されているスレッドと同じ物理コアにあるとしましょう-そのスレッドはひどく遅くなります。
David Schwartz

1
そして、もう1つだけです:x86 CPU(今日最も人気のあるCPU)では、compare_exchange_weak操作は、失敗した場合でも、常に書き込み操作です(比較が失敗した場合、読み取った同じ値を書き戻します)。したがって、2つのコアが両方ともwait同じセマフォに対してループしていると仮定します。どちらも同じキャッシュラインにフルスピードで書き込みを行っているため、コア間バスが飽和して他のコアのクロール速度が低下する可能性があります。
David Schwartz

@DavidSchwartzよろしくお願いします。「... CPUのループ予測...」の部分がよくわかりません。2番目のものに同意しました。どうやら3番目のケースが発生する可能性がありますが、ユーザーモードからカーネルモードへの切り替えとシステムコールを引き起こすミューテックスと比較すると、コア間の同期は悪化していません。
ジェフリー2017

1
ロックフリーのセマフォなどはありません。ロックフリーであることの全体的な考え方は、ミューテックスを使用せずにコードを書くことではなく、スレッドがまったくブロックしないコードを書くことです。この場合、セマフォの本質は、wait()関数を呼び出すスレッドをブロックすることです。
カルロウッド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.