0

Given this example code taken and modified from cplusplus.com

#include "stdafx.h"
// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready)
    {
        std::cout << "waiting for unlock";
        cv.wait(lck);
    }

    std::cout << "thread " << id << '\n';
    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    cv.notify_one();
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_one();
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i<10; ++i)
        threads[i] = std::thread(print_id, i);

    std::cout << "10 threads ready to race...\n";
    go();                       // go!

    for (auto& th : threads) th.join();

    return 0;
}

Does this properly block each thread until one is woken up by the notify_one() call? or is this essentially unlocking all threads?

I am attempting to start all the threads up at the same time, then block until the previous one finishes. The order is not important, but i can't have them executing the meat of the code all at the same time.

Cubbi
  • 46,567
  • 13
  • 103
  • 169
Jason
  • 2,147
  • 6
  • 32
  • 40
  • 1
    Using condition variables there is no way of knowing which one thread will be woken up when you call `notify_one`. There's also no way of enforcing ordering using condition variables. You might as well just use a simple mutex and a variable saying what thread should run which the threads poll (using the mutex and a random short sleep). – Some programmer dude Jul 11 '16 at 17:42

2 Answers2

2

Your code works in the sense that no two worker threads can execute this code:

  std::cout << "thread " << id << '\n';
  std::this_thread::sleep_for(std::chrono::milliseconds(2000));
  cv.notify_one();

concurrently, but this is not entirely because your use of std::condition_variable unblocks the worker threads one at a time. In fact, you haven't really changed the behavior from the original cplusplus.com example. In both programs the critical section of code is protected by a locked mutex, which guarantees that only one thread can hold the lock at any time.

Your use of std::condition_variable to "serialize" the execution of the threads does not by itself prevent concurrent execution because calling notify_one() doesn't guarantee that only one thread is released from wait(). The reason is that threads are allowed to spuriously return from wait() without any notification:

The function will unblock when signaled by a call to notify_one() or a call to notify_all(), or spuriously.

In a sense you are being saved by the mutex. If you wrote your worker function like this:

void print_id(int id) {
    {
        std::unique_lock<std::mutex> lck(mtx);
        while (!ready)
        {
            std::cout << "waiting for unlock";
            cv.wait(lck);
        }
    }

    std::cout << "thread " << id << '\n';
    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
    cv.notify_one();
}

This code unlocks the mutex as soon as the thread is unblocked. This would not be safe because multiple threads can reach your critical section without the protection of the locked mutex.

So I think your program does what you want - runs a section of code in many threads without concurrency - but perhaps not for the reason you expect.


The idiomatic way of blocking on a condition is to define a predicate test that passes when the thread is ready to proceed but fails otherwise, and check it in a loop:

std::unique_lock<std::mutex> lock(mutex);
while (!predicate)
    condition.wait(lock);

This idiom handles spurious wake-ups properly because the predicate test will fail and the thread will call wait() again.

Although your program looks very similar to this, it doesn't quite do this because your predicate allows all the threads to proceed, not just one, and that isn't what you stated that you want. It turns out to work anyway because of the locked mutex.

You could unblock your threads one at a time in order by changing your predicate test so that it passes only for the next thread, and by using notify_all():

#include <atomic>
...
std::atomic<int> ready(-1);

void print_id(int id) {
   {
      std::unique_lock<std::mutex> lck(mtx);
      while (ready != id)
         cv.wait(lck);
   }

   std::cout << "thread " << id << '\n';
   std::this_thread::sleep_for(std::chrono::milliseconds(2000));
   ++ready;
   cv.notify_all();
}

void go() {
   ready = 0;
   cv.notify_all();
}

int main()
{
   std::thread threads[10];
   // spawn 10 threads:
   for (int i = 0; i<10; ++i)
      threads[i] = std::thread(print_id, i);

   std::cout << "10 threads ready to race...\n";
   go();                       // go!

   for (auto& th : threads) th.join();

   return 0;
}

Note how this predicate test ready != id only passes when the thread should proceed. I also used a std::atomic<int> for ready so that notification does not require locking the mutex.

This revised predicate code is correct, but one drawback is that we have to change notify_one() to notify_all() to make sure that we wake up the next thread. This wakes up all the threads only to put all but one of them back to waiting, which costs a little bit of performance. One way to optimize this would be to create N condition_variable instances (e.g. in an array) and have each thread wait on its own condition_variable instance.

Community
  • 1
  • 1
rhashimoto
  • 15,650
  • 2
  • 52
  • 80
  • Can you suggest how to modify my main/print_id function to appropriately lock and wait so that the threads: 1. Run in order 2. lock and wait in a safe manner 3. Run one at a time – Jason Jul 12 '16 at 12:33
  • Your program already does (2) and (3). Your question says "order is not important" - are you changing that requirement? – rhashimoto Jul 12 '16 at 14:59
  • Based on your original comments, i gathered that while my program was indeed blocking, it wasn't doing so appropriately. I was looking for what you considered the "better way". While order is not a requirement, it's a nice to have. So if it is possible to do so in order, that'd be great. – Jason Jul 12 '16 at 15:22
  • I extended my answer to show how you can unblock the threads in order. I'm not sure exactly what you're trying to accomplish, though, because running your tasks sequentially and in order does not require or benefit from multithreading. – rhashimoto Jul 12 '16 at 17:45
  • Your updated answer is what i was looking for. The ready value compared against an ID was something i had considered but had not sorted out a good way to do it. Much appreciated. – Jason Jul 12 '16 at 17:50
0

After cppreference:

void notify_one();

If any threads are waiting on *this, calling notify_one unblocks one of the waiting threads.

So it's unlocking just one thread and you cannot say, which one. In order to unblock all threads you need to use void notify_all();

If you're attempting to run all threads at once you should use notify_all() in go() and then notify_one() for sequential run in print_id(int)

Remember that even when using notify_all() all threads will try to lock lck, but only one will succeed.

paweldac
  • 1,144
  • 6
  • 11