Search code examples
c++multithreadingc++11semaphorecondition-variable

Can std::condition_variables be used as counting semaphores?


This is a follow-up to Can C++11 condition_variables be used to synchronize processes?.

Can std::condition_variable objects be used as counting semaphores?

Methinks not because the object seems bound to a std::mutex, which implies it can only be used as a binary semaphore. I've looked online, including here, here, and here, but can't find reference or example of using these objects as counting semaphores.


Solution

  • Yes.

    struct counting_sem {
      counting_sem(std::ptrdiff_t init=0):count(init) {}
      // remove in C++17:
      counting_sem(counting_sem&& src) {
        auto l = src.lock(); // maybe drop, as src is supposed to be dead
        count = src.count;
      }
      counting_sem& operator=(counting_sem&& src) = delete;
      void take( std::size_t N=1 ) {
        if (N==0) return;
        auto l = lock();
        cv.wait(l, [&]{
          if (count > 0 && count < (std::ptrdiff_t)N) {
            N -= count;
            count = 0;
          } else if (count >= (std::ptrdiff_t)N) {
            count -= N;
            N = 0;
          }
          return N == 0;
        });
      }
      void give( std::size_t N=1 ) {
        if (N==0) return;
        {
          auto l = lock();
          count += N;
        }
        cv.notify_all();
      }
      // reduce the count without waiting for it
      void reduce(std::size_t N=1) {
        if (N==0) return;
        auto l = lock();
        count -= N;
      }
    private:
      std::mutex m;
      std::condition_variable cv;
      std::ptrdiff_t count;
    
      auto lock() {
        return std::unique_lock<std::mutex>(m);
      }
      auto unlocked() {
        return std::unique_lock<std::mutex>(m, std::defer_lock_t{});
      }
    };
    

    Code not tested or compiled, but the design is sound.

    take(7) is not equivalent to for(repeat 7 times) take(): instead, it takes as much as it can then blocks if that wasn't enough.

    Modifying so that it doesn't take anything until there is enough is easy:

          if (count >= (std::ptrdiff_t)N) {
            count -= N;
            N = 0;
          }