Implementing boost::barrier in C++11

12,907

Solution 1

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;
};

Solution 2

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; });
        }
    }
};

Solution 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; });
                }
            }
        }
    };  
Share:
12,907

Related videos on Youtube

h4lc0n
Author by

h4lc0n

Game Developer, also worked as a Software Engineer specialized in porting videogames to PSVita, PS3, PS4, Switch, XB1, XBS, MacOSX and other platforms, currently working at Larian Studios in Barcelona, Spain.

Updated on September 11, 2022

Comments

  • h4lc0n
    h4lc0n over 1 year

    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).

  • Nobody moving away from SE
    Nobody moving away from SE almost 9 years
    I like the fact that this works over several generations.
  • Levi Morrison
    Levi Morrison about 8 years
    Note that this implementation is suitable only for a one-time use barrier but is safe from spurious wake-ups.
  • Levi Morrison
    Levi Morrison about 8 years
    This implementation is reusable and safe from spurious wake-ups.
  • thegreatcoder
    thegreatcoder over 5 years
    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.
  • jamshid
    jamshid over 4 years
    I believe there's a typo, should be lLock(mMutex) not lLock{mMutex}.
  • Romeo Valentin
    Romeo Valentin over 4 years
    @jamshid No it's not a typo, have a look at the examples on list initialization
  • murfel
    murfel over 3 years
    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
  • murfel
    murfel over 3 years
    See also the original Boost implementation and API documentation. Also, this SO answer explains the need for mGeneration.
  • murfel
    murfel over 3 years
    It's better to release the lock before calling notify_all() (that's what the Boost implementation does), see this SO answer.