1

I am dealing with the following (maybe silly) issue: I am trying to synchronise two threads (A and B), and I want to block the thread A until a condition is set to true by the thread B.

The "special" thing is that the condition is checked on a thread-safe object (for instance, let's consider it to be a std::atomic_bool).

My naive approach was the following:

// Shared atomic object
std::atomic_bool condition{false};

// Thread A
// ... does something
while(!condition.load()) ;  // Do nothing
// Condition is met, proceed with the job

// Thread B
// ... does something
condition.store(true);      // Unlock Thread A

but, as far as I have understood, the while implies an active wait which is undesirable.

So, I thought about having a small sleep_for as the body of the while to reduce the frequency of the active wait, but then the issue becomes finding the right waiting time that does not cause waste of time in case the condition unlocks while thread A is sleeping and, at the same time, does not make the loop to execute too often. My feeling is that this is very much dependant on the time that thread B spends before setting the condition to true, which may be not predictable.

Another solution I have found looking on other SO topics is to use a condition variable, but that would require the introduction of a mutex that is not really needed.

I am perhaps overthinking the problem, but I'd like to know if there are alternative "standard" solutions to follow (bearing in mind that I am limited to C++11), and what would be the best approach in general.

halfer
  • 19,824
  • 17
  • 99
  • 186
Davide
  • 387
  • 2
  • 11
  • 6
    Based on your description of what you want, a condition variable is probably the right answer. – Jerry Coffin Oct 07 '22 at 17:17
  • The quest for the lock-free unicorn fairy almost always ends in tears. The described behavior is perfectly suited for a regular, non-atomic bool, a mutex, and a condition variable, with well-defined semantics. – Sam Varshavchik Oct 07 '22 at 17:23
  • 1
    C++20 would give you `std::latch` and `std::barrier`. IIRC you'd need to implement them yourself or fall back to osdependent functionality to do the same in a earlier version of the standard... – fabian Oct 07 '22 at 17:31
  • 3
    `std::atomic` has `.wait()` in C++20. – HolyBlackCat Oct 07 '22 at 17:36
  • You usually need mutex to synchronize shared data access anyway. If you think you really don't, with c++11 you can wrap mutex + condition_variable into some kind of [event](https://stackoverflow.com/a/8129013/4074081) class, but again usually this is incorrect approach (that's why such primitive was not indroduced in standard library). – dewaffled Oct 07 '22 at 17:39
  • @SamVarshavchik the atomic bool is an example I chose for simplicity to explain the problem. There are classes that are designed to be thread safe (maybe using locks inside), so I am wondering whether the cost for an extra, unneeded mutex is still the best option. – Davide Oct 07 '22 at 17:40
  • 3
    Re, "condition variable...would require...a mutex that is not really needed." You might want to re-check that assumption. The whole point of a condition variable is to let one thread wait until another thread has put _shared data_ into some desirable state. Are you so sure that you don't need a mutex to access the shared data? [P.S., Even if you really _don't_ need the mutex, are you so sure that using one anyway would have an unacceptably detrimental impact on the performance of your program?] – Solomon Slow Oct 07 '22 at 17:44
  • 1
    A mutex is a good way to get OS-assisted sleep/wake (instead of a busy-wait spin-loop which you correctly want to avoid), especially before C++20 `std::atomic` `.wait()`. What performance goals do you have that you think a condition variable wouldn't satisfy? Like just low total overhead? Yeah a condition variable seems a bit clunky to me, with a lot of moving parts and a lot of calls. But it's better than waking up a sleeping CPU core (or steal it from another thread with a context switch) to pool a memory location every `sleep_for` milliseconds! That's horrible, or very slow to notice – Peter Cordes Oct 07 '22 at 17:56
  • 1
    Thanks for all your comments. I was just trying to understand whether using the mutex + condition variable is the best approach in general, or whether there may be other than the ones I mentioned that may be better. I was not try to make a point about the efficiency (which I'm pretty sure it would not be affected as you all mentioned). Sorry, I should have probably clarified it better in my question. – Davide Oct 07 '22 at 18:03
  • (typo in my last comment: *poll* a memory location.) – Peter Cordes Oct 08 '22 at 04:11

3 Answers3

2

Your use case is simple and there are many ways to implement that. The first recommendation would be to make use of condition variable. But it seems from your question that you would like to avoid that because of mutex. I don't have any profiling data for your use case, but mutex isn't costly for your use case.

In a multi-threaded environment, at some point of time, you would need some techniques to protect shared access and modification of data. You would probably need mutexes for that.

You could go for condition variable approach. It is by the standard, and it also provides function to notify all the threads as well, if your use case scales in future. Also, as you mentioned about "time", condition_variable also comes with variations of wait* functions where the condition could be in terms of "time". It can wait_for or wait_until a certain time as well.

About the while loop and a sleep_for approach, blocking a thread from execution and then rescheduling it again isn't that cheap if we are counting in terms of milliseconds. The condition variable approach would be better suited in this case, rather than having the while loop and an explicit call to sleep_for.

void
  • 338
  • 5
  • 19
1

Sorry, condition variables are the way to go here.

The mutex is being used as a part of the condition variable, not as a traditional mutex. And barring some strange priority inversion situation, it shouldn't have much cost.

Here is a simple "farm gate". It starts shut, and can be opened. Once opened, it can never be shut again.

struct gate {
  void open_gate() {
    auto l = lock();
    gate_is_open = true;
    cv.notify_all();
  }
  void wait_on_gate() const {
    auto l = lock();
    cv.wait(l, [&]{ return gate_is_open; });
  }
private:
  auto lock() const { return std::unique_lock{m}; }
  mutable std::mutex m;
  bool gate_is_open = false;
  std::condition_variable cv;
};

which you'd use like this:

// Shared gate
gate condition;

// Thread A
// ... does something
condition.wait_on_gate();  // Do nothing
// Condition is met, proceed with the job

// Thread B
// ... does something
condition.open_gate(); // Unlock Thread A

and there we have it.

In there is std::latch. Start the counter at 1, decrement it when the gate opens, and the other thread waits on the latch.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

How about using some sort of a sentinel value to check if the conditions of thread B are true to unlock thread A and synchronize both of them once the condition is met.

ByteTh1ef
  • 71
  • 5