std::condition_variable
El nuevo std::condition_variable
en C++ 11 es un bicho versátil pero por lo menos para mi un fuente de confusión.
Principalmente tiene dos roles, uno funciona en un hilo recibiendo notificaciones, y la otra para transmitir notificaciones desde un un hilo a otro.
1. El hilo espera (`wait()`) hasta que recibe un 'señal' sin mantener un bloqueo de un candado (**`std::mutex`**)
2. Cuando el hilo recibe el 'señal' se despierta y adquiere el bloqueo de un candado otra vez.
El std::condition_variable
siempre trabaja en conjunto e un std::mutex
via un std::unique_lock
, en simple caso solo para proteger una condición. Pero también podemos utilizar para proteger otros recursos, mas de eso abajo en ejemplo 3.
std::condition_variable
std::mutex awakeThreadMutex;
std::condition_variable signal_thread;
void waitOnSignal()
{
cout << "Thread created" << el;
std::unique_lock<std::mutex> locker(awakeThreadMutex);
signal_thread.wait(locker);
cout << "Thread signaled" << el;
}
void startThreadAndSignal()
{
std::thread t(waitOnSignal);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
signal_thread.notify_one(); //Or signal_thread.notify_one(); it is the same here with only one thread
t.join(); //Wait for thread to terminate, if not undefined/bad things happens
}
std::condition_variable
El ejemplo anterior, tiene una problema, hay una posibilidad que el hile esperando con wait()
sea despertado sin ninguno otro hilo ha señalado.
En ingles se llama 'spurious wake up'
Así la recomendación es siempre usa una condición con un bucle que controla si realmente fue señalada. El alternativo con predicado, en teoría puede ser más eficaz si el implementador de la librería consigue algo mejor que un bucle...
std::atomic<bool> threadSignaled = false;
void waitOnSignal2()
{
cout << "Thread created in waitOnSignal2" << el;
std::unique_lock<std::mutex> locker(awakeThreadMutex);
Alternativo con bucle
while (!threadSignaled) //Guarantees to protect if a spurious wake up happens
{
signal_thread.wait(locker);
}
Alternative con wait() usando predicado
//Alternative using wait() with predicate
signal_thread.wait(locker, []() { return threadSignaled.load(); });
cout << "Thread signaled in waitOnSignal2" << el;
}
void startThreadAndSignal2()
{
//Version safe against spurious wake ups
std::thread t2(waitOnSignal2);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
threadSignaled = true;
signal_thread.notify_one(); //Or signal_thread.notify_one(); it is the same here, first thread is gone
t2.join(); //Wait for thread to terminate, if not undefined/bad things happens
}
std::unique_lock
y no el std::lock_guard
El std::condition_variable::wait
requiere un std::unique_lock
porque mientras está esperando se libera el bloque, y despertando se adqiure otra vez.
El std::lock_guard
no tiene esta funcionalidad. Pero el std::unique_lock
tiene el mismo comportamiento que std::lock_guard
y se libera el bloqueo cuando sale del ámbito.
Ya hemos visto como hilos puede señalar una al otro, pero el std::condition_variable
no solo sirve para esto. También si los hilos comparten recursos y cuando necesitamos coordinar el acceso entre ellos los std::condition_variable
nos sirve en combinaciónes para señalar y proteger que el recurso ya está libre para otro hilo.
En el siguiente ejemplo creamos dos hilos que comparte un recurso, un vector. Cada uno de ellos trabaja con el vector protegido y cuando terminan su trabajo, señala para el otro hilo que toma las riendas del recurso y hace su trabajo con el vector protegido. Cuando termina su trabajo, le vuelve a señalar para el otro hilo, y así siguen.
Nota que usamos dos std::condition_variable
, uno para comunicar or señalar en una direcion y receiber desde la misma, y otra para comunicar en la otra direcion.
¡Ver el ejemplo!
std::mutex alternateThreadsMutex;
std::condition_variable runThread1condition;
std::atomic<bool> runThread1 = false;
std::atomic<bool> runThread2 = false;
std::condition_variable runThread2condition;
std::vector<int> threadWork;
void thread1()
{
while (true)
{
std::unique_lock<std::mutex> lock(alternateThreadsMutex);
runThread1condition.wait(lock, []() { return runThread1.load(); });
auto workSize = threadWork.size();
if (workSize < 1000)
threadWork.push_back(1);
runThread1 = false;
runThread2 = true;
lock.unlock();
runThread2condition.notify_one();
if (++workSize >= 1000)
{
break;
}
}
}
void thread2()
{
while (true)
{
std::unique_lock<std::mutex> lock(alternateThreadsMutex);
runThread2condition.wait(lock, []() { return runThread2.load(); });
auto workSize = threadWork.size();
if (workSize < 1000)
threadWork.push_back(1);
runThread2 = false;
runThread1 = true;
lock.unlock();
runThread1condition.notify_one();
if (++workSize >= 1000)
{
break;
}
}
}
void alternateThreads()
{
std::thread t1(thread1);
std::thread t2(thread2);
runThread2 = true;
runThread2condition.notify_one();
t1.join();
t2.join();
cout << threadWork.size() << el;
}