18

I've been trying to get a project rid of every boost reference and switch to pure C++11.

At one point, thread workers are created which wait for a barrier to give the 'go' command, do the work (spread through the N threads) and synchronize when all of them finish. The basic idea is that the main loop gives the go order (boost::barrier .wait()) and waits for the result with the same function.

I had implemented in a different project a custom made Barrier based on the Boost version and everything worked perfectly. Implementation is as follows:

Barrier.h:

class Barrier {
public:
    Barrier(unsigned int n);
    void Wait(void);
private:
    std::mutex counterMutex;
    std::mutex waitMutex;

    unsigned int expectedN;
    unsigned int currentN;
};

Barrier.cpp

Barrier::Barrier(unsigned int n) {
    expectedN = n;
    currentN = expectedN;
}

void Barrier::Wait(void) {
    counterMutex.lock();

    // If we're the first thread, we want an extra lock at our disposal

    if (currentN == expectedN) {
        waitMutex.lock();
    }

    // Decrease thread counter

    --currentN;

    if (currentN == 0) {
        currentN = expectedN;
        waitMutex.unlock();

        currentN = expectedN;
        counterMutex.unlock();
    } else {
        counterMutex.unlock();

        waitMutex.lock();
        waitMutex.unlock();
    }
}

This code has been used on iOS and Android's NDK without any problems, but when trying it on a Visual Studio 2013 project it seems only a thread which locked a mutex can unlock it (assertion: unlock of unowned mutex).

Is there any non-spinning (blocking, such as this one) version of barrier that I can use that works for C++11? I've only been able to find barriers which used busy-waiting which is something I would like to prevent (unless there is really no reason for it).

h4lc0n
  • 2,590
  • 5
  • 27
  • 40

3 Answers3

32
class Barrier {
public:
    explicit Barrier(std::size_t iCount) : 
      mThreshold(iCount), 
      mCount(iCount), 
      mGeneration(0) {
    }

    void Wait() {
        std::unique_lock<std::mutex> lLock{mMutex};
        auto lGen = mGeneration;
        if (!--mCount) {
            mGeneration++;
            mCount = mThreshold;
            mCond.notify_all();
        } else {
            mCond.wait(lLock, [this, lGen] { return lGen != mGeneration; });
        }
    }

private:
    std::mutex mMutex;
    std::condition_variable mCond;
    std::size_t mThreshold;
    std::size_t mCount;
    std::size_t mGeneration;
};
Jinfeng Yang
  • 336
  • 3
  • 2
  • I like the fact that this works over several generations. – Nobody moving away from SE Jul 03 '15 at 11:43
  • 2
    This implementation is reusable and safe from spurious wake-ups. – Levi Morrison Mar 10 '16 at 21:58
  • I believe there's a typo, should be lLock(mMutex) not lLock{mMutex}. – jamshid Sep 11 '19 at 15:30
  • 2
    @jamshid No it's not a typo, have a look at the examples on [`list initialization`](https://en.cppreference.com/w/cpp/language/list_initialization) – Romeo Valentin Nov 05 '19 at 19:54
  • If you're curious why `std::unique_lock` rather than `std::lock_guard`, it's because it's relockable. We need to release the lock when waiting on the conditional variable. [See this SO answer](https://stackoverflow.com/a/20516876/3478131) – murfel Dec 04 '20 at 06:54
  • See also [the original Boost implementation](https://www.boost.org/doc/libs/1_74_0/boost/thread/barrier.hpp) and [API documentation](https://www.boost.org/doc/libs/1_74_0/doc/html/thread/synchronization.html#thread.synchronization.barriers). Also, [this SO answer](https://stackoverflow.com/a/26190916/3478131) explains the need for `mGeneration`. – murfel Dec 04 '20 at 07:15
  • It's better to release the lock before calling `notify_all()` (that's what the Boost implementation does), see [this SO answer](https://stackoverflow.com/a/17102100/3478131). – murfel Dec 04 '20 at 07:53
17

Use a std::condition_variable instead of a std::mutex to block all threads until the last one reaches the barrier.

class Barrier
{
private:
    std::mutex _mutex;
    std::condition_variable _cv;
    std::size_t _count;
public:
    explicit Barrier(std::size_t count) : _count(count) { }
    void Wait()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if (--_count == 0) {
            _cv.notify_all();
        } else {
            _cv.wait(lock, [this] { return _count == 0; });
        }
    }
};
nosid
  • 45,370
  • 13
  • 104
  • 134
  • 7
    Note that this implementation is suitable only for a one-time use barrier but is safe from spurious wake-ups. – Levi Morrison Mar 10 '16 at 21:51
  • Suppose I have three threads that need to wait on the barrier and I initialize the barrier as Barrier barrier(2); this code does not seem to work. – thegreatcoder Dec 07 '18 at 23:58
3

Here's my version of the accepted answer above with Auto reset behavior for repetitive use; this was achieved by counting up and down alternately.

    /**
    * @brief Represents a CPU thread barrier
    * @note The barrier automatically resets after all threads are synced
    */
    class Barrier
    {
    private:
        std::mutex m_mutex;
        std::condition_variable m_cv;

        size_t m_count;
        const size_t m_initial;

        enum State : unsigned char {
            Up, Down
        };
        State m_state;

    public:
        explicit Barrier(std::size_t count) : m_count{ count }, m_initial{ count }, m_state{ State::Down } { }

        /// Blocks until all N threads reach here
        void Sync()
        {
            std::unique_lock<std::mutex> lock{ m_mutex };

            if (m_state == State::Down)
            {
                // Counting down the number of syncing threads
                if (--m_count == 0) {
                    m_state = State::Up;
                    m_cv.notify_all();
                }
                else {
                    m_cv.wait(lock, [this] { return m_state == State::Up; });
                }
            }

            else // (m_state == State::Up)
            {
                // Counting back up for Auto reset
                if (++m_count == m_initial) {
                    m_state = State::Down;
                    m_cv.notify_all();
                }
                else {
                    m_cv.wait(lock, [this] { return m_state == State::Down; });
                }
            }
        }
    };  
Michael Sutton
  • 298
  • 2
  • 9