Anytime you use a condition variable, you also (unavoidably) use a mutex. There's simply no getting away from that. You can't use a cv without a mutex, because they're simply not designed to work that way.
You could, for one possibility, make ready an atomic<bool>, which would guarantee that the change in its value would propagate to the other thread correctly. You'd still need the mutex for the cv though, so you might as well use the mutex to protect ready as well (in which case, you don't need to make ready atomic, because the mutex guarantees propagation, just like making it atomic does).
Since you've tagged C++20, I'll also mention an alternative: you could consider an atomic_flag instead of a condition variable. For example, we could rewrite your program a bit like this:
#include <atomic>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
std::atomic_flag ready {};
std::atomic_flag processed {};
std::string data;
void worker_thread()
{
// Wait until main() sends data
ready.wait(false);
ready.clear();
std::cout << "Worker thread is processing data\n";
data += " after processing";
processed.test_and_set();
processed.notify_one();
std::cout << "Worker thread signals data processing completed\n";
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
ready.test_and_set();
ready.notify_one();
std::cout << "main() signals data ready for processing\n";
processed.wait(false);
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
[As a side note: std::atomic_flag has existed since C++11, but the notify_one and wait were added in C++20.]
This can often reduce overhead compared to using a condition variable.
Also note that we could use only one atomic_flag, if we really wanted. main would set it, and notify one, then wait for it to be cleared, before doing the final processing. The worker thread would wait for it to be set, modify the data, then clear it and notify one to let main know it was done:
#include <atomic>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
std::atomic_flag ready {};
std::string data;
void worker_thread()
{
// Wait until main() sends data
ready.wait(false);
std::cout << "Worker thread is processing data\n";
data += " after processing";
ready.clear();
ready.notify_one();
std::cout << "Worker thread signals data processing completed\n";
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
ready.test_and_set();
ready.notify_one();
std::cout << "main() signals data ready for processing\n";
ready.wait(true);
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
Race Conditions
In the comments it has been asserted that my second paragraph is incorrect, and making ready an std::atomic<bool> would result in a race condition. A long string of comments has resulted in a great deal of heat, but little or no light, so I'm going to go into a ridiculous level of detail here in the hope that changing that.
Let's start by reviewing the original code at a very basic level:
The main thread:
- initializes
ready and processed to false
- spawns a worker thread
- generates some data for the worker thread to process
- sets
ready to true
- waits for
processed to become true
- prints out the result
- exits the process
The worker thread:
- waits for
ready to become true
- processes the data
- sets
processed to true
- exits the thread
In particular, note that the program:
- processes exactly one (1) batch of data one (1) time.
- initializes
ready and processed to false
ready is only ever written to by one thread, one time
processed is only ever written to by one thread, one time
So, after ready becomes true, it remains true until the process exits. Likewise, one processed becomes true, it remains true until the process exits. I'll repeat for emphasis, because this is important: there is no circumstance under which either ready or processed will transition from true to false. Both start out false, transition to true, then remain true until the process exits.
Second major point: there is only ever one main thread and one worker thread.
The first claim of a race condition was that:
Your second paragraph is incorrect though. Making ready atomic is not enough to ensure its value propagates correctly to the other thread for the reason I explained in my comment above.
At least as they're defined in C++, this is false. A variable that's not atomic may not propagate from one thread to another--for example, thread A can change a bool from false to true, and thread B can later read that variable, but still see the old value instead of the updated value.
If, however, the variable is atomic, that can't happen. It's guaranteed that if one thread updates a value of an atomic variable, any subsequent read by any thread will always see the updated value.
That's what propagation means, and (in C++) atomic variables guarantee propagation.
Although that mistake was never admitted, that led to another attempt at another, different claim of a race condition:
Using an atomic by itself does not force propagation because it won't unblock a thread that is waiting for the value to change. That's why we're talking about using it in conjunction with a condition variable. Read my first comment again, particularly the "unlock and wait" part. The code will not work because a thread observed the atomic, decides to sleep, another thread modifies the atomic, the first thread then goes to sleep to wait for something that has already happened. You need an atomic "unlock and wait" operation and that's what the mutex is for -- to have something to unlock.
First of all, this completely misuses "propagation" to mean something almost, but not quite, completely different from what it actually means.
Second, it seems to be based on using some threading primitive that would wake a thread when a write is done to an atomic, but with no knowledge of the value contained in that atomic. At least, that's the only way I can make any sense of: "[...] the first thread then goes to sleep to wait for something that has already happened."
That leads to yet another problem though: the "goes to sleep to wait" part seems to presuppose that there's some primitive to sleep until there's a write to the atomic variable. Since this entire line of reasoning starts off by asserting that no such capability exists, I'm baffled at how this can possibly make any sense.
In reality, you could (for only one possibility) basically do something like a spin-lock:
while (!ready)
std::this_thread::yield();
Depending on the situation (especially if you expect that to take very long) there are other ways to do the job that can and will be better, but that's sufficient to illustrate the basic point that it's entirely possible for a thread to wait until an atomic variable contains a specific value, without creating any race condition, or using an argument that depends on your using a primitive that it starts out by asserting doesn't exist.
Since that argument didn't make much sense, I tried to guess at something he might be trying to get at that wasn't self-contradictory. The possibility that occurred to me was what's called the ABA problem. This can occur with code like the loop above, where we get a sequence like this this:
ready is set to true
ready is set back to false
- thread C checks whether
ready is true, and misses the fact that it was true for a short time
If the intent is for thread C to react to every change in ready, then this can be a real problem.
As a bare minimum, however, this requires that there is some circumstance under which ready can/will be changed from true to false--which (as already noted) simply can't happen in any of the code considered here. Further, since we only process a single batch of data going from the main thread to the worker thread, we'd have to postulate some rather different program that did some rather different thing before it made any sense to consider doing that at all.
Summary
Changing ready from bool to std::atomic<bool> would ensure that writes to ready propagated between threads, even without a mutex. And if you did so, you'd no longer need to use a mutex to protect ready from changes by multiple threads simultaneously, because atomic variables guarantee defined behavior when modified from multiple threads without other synchronization.
But when you're using a condition variable, you need to use a mutex. So when you're doing that, there's no real point in making ready atomic--a condition variable requires you to use a mutex, regardless, and synchronizing with that mutex assures propagation of its value, even without its being atomic.