Condition(al) variable

What happens in this piece of code?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

My expectation is this way, albeit I'm almost sure it's not completely correct:
The program, as useful, starts from the main() function/thread, the worker thread is the called on line 35. After that point, the program's control goes to the worker_thread function. A unique_lock is created there and the condition variable cv is going to wait (some situation like sleeping until an event wakes it up) on line 17. So the program's control gets back to main() on line 37 where it adds a string into data. Inside the following local block, a lock_guard is defined and the bool value ready gets true. We then have some data as output by std::cout. By exiting the block, on line 43, the lock on the mutex, lk, is unlocked automatically. cv will send a notify_one signal, what cv. wait on line 17 is waiting for, therefore control goes there and we have some output on line 20 followed by altering data on the next line. Moving on, processed gets true and we cout a message and next manually unclock the lk (on line 29). The control's still there, hence by cv.notify_one, it goes back to line 46 from the line 30. Afterwards, there's another local block (47-50), in which lk(m) is defined and cv falls into a wait. But since processed is already trued, the control goes out of the block and cout a message on line 51. Thereafter, the worker thread is finished followed by the main thread. And done!

How much right, please?
Last edited on
Your understanding of how threads work is completely wrong. When you create a thread a new control flow is created that runs independently from the main thread. Using locks and condition variables just causes the caller thread to wait, it doesn't cause control to switch back and forth between the threads, although outwardly that may appear to happen.

Example of what happens when two threads run without any synchronization:
1
2
3
4
5
6
7
std::thread t([](){
    for (int i = 0; i < 100000; i++)
        std::cout << i << std::endl;
});
for (int i = 0; i < 100000; i++)
    std::cout << -i << std::endl;
t.join();
It's absolutely impossible to know what this program will print, because the threads are sending output in parallel without any coordination. There's no control switching back and forth, there's two independent control flows producing output.
OK, I knew there must have been something wrong. I edit my explanation the following way, but please bear in mind that except for multi-threading, my intention is to know the condition variable technique too.

The program, as usual, starts from the main() function/thread, the worker thread is called on line 35 and it's going to work (starting from the worker_thread function) alongside the main thread. A unique_lock is created there and the condition variable cv is going to wait (some situation like sleeping until an event wakes it up) on line 17, so the worker thread stops here whereas the main thread has already been working printing "example data" on line 37 then inside the following local block, a lock_guard is defined and the bool value ready gets true.
We then have some data as output by std::cout. By exiting the block, on line 43, the lock on the mutex, lk, is unlocked automatically. cv will send a notify_one signal, what it is waiting for on line 17. Therefore we have some output on line 20 followed by altering data on the next line. Moving on, processed gets true and we cout a message and next manually unlock the lk (on line 29), while the main thread has entered a local block waiting for the worker thread's notification after creating the lock lk(m). cv.notify_one on line 30 causes itself in the main thread to wake up and go on to print some data on line 51. Thereafter, the worker thread is finished followed by the main thread. And done!
How right is it now, please?
Last edited on
When it comes to threading don't expect any sort of order.

The first wrong assumption is 'worker thread is called on line 35'. No, it is not called. It is prepared to start. Means that on line 46 the worker_thread isn't even started or is already finished. These are possible [extreme] scenarios. So as helios stated it is impossible to say what state both thread are in at a certain time.

It also important to know that notify_one/all() has no effect when no condition_variable is waiting. Therefore the bool variable ready/processed are introduced. With this you get out of the wait state even when notify_... has no effect.
Thank you for your reply. Since the idea is very sophisticated it's possible for one like me not to comprehend that at the beginning of using it quickly.
So to figure this example project out appropriately, will you please tell me what happens in this program when we run it until it's finished.
As another endeavor:
On line 35, the worker thread is prepared to start given its argument, the worker_thread function, which is called. On line 16 a unique lock is created using the mutex m, then on the following line, the condition variable, cv, makes the thread sleep until it's woken up. So in this case the worker thread is stopped while the main thread is still (already) running. It executes instructions on line 37 through 44 where it will notify the waiting thread (here the worker thread) to wake up. When that thread is woken up, it checks the predicate ready, which is now true, so it executes the instructions onwards up to line 30, where it notifies some slept/stopped/waiting thread to wake up! On the other hand, the main thread (may) has/have been going on until it falls into wait on line 49, where it has been waiting for a notification from a condition variable. As mentioned earlier, that notification is already launched on line 30, so the condition variable processes processed, which is now true, and so on.

Is there still something wrong in my assumption? If so, then let me know how you interpret this project, please.
What you described is one possible processing sequence. There are many others.

You should consider it from the standpoint of the data (line 9). When is it safe to access that data.
Answer: Whenever the shared mutex is locked.

On line 37 it is safe to modify data because at this point of time it is guaranteed that worker_thread will not go beyond 17.

Line 41/44 enables the worker_thread to go beyond line 17 where data will be modified. So after line 44 It is not longer safe to access data without a lock on the mutex m.
It will be safe after line 48 for the main thread.

You need a conditional variable to notify/wait until there is something to do with the data. This prevents unnecessary cycles that otherwise just consumes processor time.
To wake up the waiting thread you need additional variables (in this case ready/processed) to ensure that the conditional variable gets out of its wait state (only the combination of line 41/44 and 24/30 will guarantee this). As mentioned earlier the notify_one/all() has only an effect when the conditional variable is waiting at that time.
It is also important to know that the wait() function of the conditional variable unlocks the mutex during its wait and as soon as it is notified locks it again. When resuming from the wait it is safe for the waiting thread to access the data.

So there are two things to consider when synchronizing threads:

1. Getting the thread out of its wait state in time.
2. Protecting the data (as short as possible).

Failing to do so may lead to a deadlock or corrupt data.

The problem with thread is that things are just have a likeliness to happen (consider the worst case scenario). You need a lock to guarantee that things happens. So it is better to consider what is happening before/during/after the lock.
Last edited on
So after line 44 It is not longer safe to access data without a lock on the mutex m.
You mean in the main thread. Yeah?

2. Protecting the data (as short as possible).
I didn't get it good enough I guess.

3) By the way, did you mean "safe" by "save"?

What you described is one possible processing sequence. There are many others.
Will you also describe another possible processing process?

The problem with thread is that things are just have a likeliness to happen (consider the worst case scenario). You need a lock to guarantee that things happens. So it is better to consider what is happening before/during/after the lock.
Do you think that multi-threading use must be best avoided except for the time we have a good number of actual cores and a good reason for using multi-threads, please?

6) And the last question (of this post) Do you also agree that that example utilizes multi-threading almost properly?


Last edited on
Will you also describe another possible processing process?
No particular scheduling of threads is guaranteed by any system. You wrote this:
On line 35, the worker thread is prepared to start given its argument, the worker_thread function, which is called. On line 16 a unique lock is created using the mutex m, then on the following line, the condition variable, cv, makes the thread sleep until it's woken up. So in this case the worker thread is stopped while the main thread is still (already) running. It executes instructions on line 37 through 44 where it will notify the waiting thread (here the worker thread) to wake up.
But the system could conceivably schedule the threads to run like this:
On line 35, the worker thread is prepared to start given its argument, the worker_thread function, which is called. On line 16 a unique lock is created using the mutex m, then on the following line, the condition variable, cv, makes the thread sleep until it's woken up. So in this case the worker thread is stopped while the main thread is still (already) running. The operating system then decides to not give any more time slices to the worker thread for a while. The main thread executes instructions on line 37 through 44 where it will notify the waiting thread (here the worker thread) to wake up. The operating system then finally decides to give the worker thread more time slices and worker_thread() executes without stopping from beginning to end, and the thread is terminated. Then the main thread continues running and eventually the program terminates.

Do you think that multi-threading use must be best avoided except for the time we have a good number of actual cores and a good reason for using multi-threads, please?
This is true in general, not just for threads. Why would you add something to a program without a good reason?
If the program can be made "better" (whatever that means in the particular context) by using threads, then you should use threads. If you use threads, you should be mindful of how you share data between them, and how you access said data, and if possible you should share as little data as possible. That last point is important not just for safety, but also for performance. The less data threads share between them, the less synchronization they'll need and the less they'll have to wait for each other.
Last edited on
The operating system then decides to not give any more time slices to the worker thread for a while. The main thread executes instructions on line 37 through 44 where it will notify the waiting thread (here the worker thread) to wake up. The operating system then finally decides to give the worker thread more time slices and worker_thread() executes without stopping from beginning to end, and the thread is terminated. Then the main thread continues running and eventually the program terminates.
This is a very good answer. I learnt much from this. Thanks. :)

If by any coincidence, the operating system decides to give the worker thread some time slices to start and execute, that thread must stop on line 17 if the main thread hasn't reached line 44 yet. Right?

If the program can be made "better" (whatever that means in the particular context) by using threads, then you should use threads. If you use threads, you should be mindful of how you share data between them, and how you access said data, and if possible you should share as little data as possible. That last point is important not just for safety, but also for performance. The less data threads share between them, the less synchronization they'll need and the less they'll have to wait for each other.
Very concise and helpful. Thanks, got it.
Last edited on
If by any coincidence, the operating system decides to give the worker thread some time slices to start and execute, that thread must stop on line 17 if the main thread hasn't reached line 44 yet. Right?
Yes.
Topic archived. No new replies allowed.