c++multithreadingc++20semaphorethread-synchronization

Am I doing something wrong with C++ std::semaphore or are they just not useful for signaling in this way?


I am working on a multi threaded program where a looping streaming thread blocks/sleeps/waits for indication from a control thread to run in its loop, and can then be 'paused' when the user disables all streams.

Usually I would use condition variables for something like this, however due to the nature of the problem (any streams being requested should allow the loop to start, and it should only stop when the last stream is requested to be disabled) I think that solution would be more complicated than I would like (not very complicated, but would need more bookkeeping at least). I'm trying to see if semaphores are a good choice for this after reading about them here:

Semaphores are also often used for the semantics of signalling/notifying rather than mutual exclusion, by initializing the semaphore with ​0​ and thus blocking the receiver(s) that try to acquire(), until the notifier "signals" by invoking release(n). In this respect semaphores can be considered alternatives to std::condition_variables, often with better performance.

I mocked up a simple example since this is my first time using C++20 std::semaphores, but this example does not work as expected:

#include <iostream>
#include <thread>
#include <semaphore>
#include <unistd.h>

std::counting_semaphore streams_sem{0};

void stream_thread(){
  while (1){
    streams_sem.acquire();
    std::cout << "Sending images!\n";
    usleep(500000);
    streams_sem.release();
  }
}

int main(int argc, char const* argv[]){
  std::thread t1(stream_thread);
  t1.detach();
  std::cout << "Main thread starting streams\n";
  streams_sem.release();
  usleep(3e6);
  std::cout << "Main thread stopping streams\n";
  streams_sem.acquire();

  return 0;
}

When run, this is the output I get:

Main thread starting streams
Sending images!
Sending images!
Sending images!
Sending images!
Sending images!
Sending images!
Main thread stopping streams
Sending images!
Sending images!
... and on

I can assume that the main thread is not ever getting scheduled in a time between the looping release() and acquire(), although I would have expected it catch it eventually (in a timely manner, a few sec tops) since the main thread is already blocked on acquire() and because of the above comparison to condition variables.

This leads me to my main (sub)questions:

  1. What kind of pitfalls may I encounter if any (besides just wasting time) by having a short (10-100uS) sleep after the release() in the looping thread? IE Could that still have a likely chance to fail to stop it, making it a bad solution?
  2. Is this an inappropriate or invalid use of semaphores? Or is there something else I could do so they would act more like a signal/condition variable with an inbuilt counter?

I would also be open to any other ways I could achieve this functionality. I am trying to avoid using condition variables for this since I believe the solution would be complicated, but if there is another synchronization method that seems like a better fit, I would love to hear about that as well.

------Edit: Relevant information------

In practice, these streams are essentially always available and protected on their own during access, so the only functionality I need here is to know if any are active (I could technically just make it a busy loop and check if each stream is active always, but want a lightweight way that would block when none are requested).


Solution

  • I can assume that the main thread is not ever getting scheduled in a time between the looping release() and acquire(), although I would have expected it catch it eventually (in a timely manner, a few sec tops) since the main thread is already blocked on acquire() and because of the above comparison to condition variables

    There's literally one instruction between the release and re-acquire, it's an unconditional jump. You're being extremely optimistic about the likelihood of the main thread getting in there.

    As I understand it, your logic is supposed to be something like:

    1. The semaphore counter represents the number of active streams
    2. The thread should keep processing streams while that number is non-zero
    3. Another thread is responsible for starting/stopping streams (incrementing/decrementing the counter)

    Nothing there suggests the worker thread should ever modify the number of streams. So semaphores are at least not a perfect fit.

    If we accept the thread is using semaphores to check for a non-zero counter, it should just acquire and immediately release, before doing the work.

    This leaves out all discussion of what these streams are and how it coordinates access to them with the main thread. This is the stuff that would normally be protected by the mutex associated with a condition variable.

    That is to say, I'm not convinced that this example shows all the shared state that needs to be synchronized, and if you have to add a mutex anyway, the semaphore doesn't end up being simpler than a regular condition variable.