Использование std :: mutex, std :: condition_variable и std :: unique_lock

У меня возникли проблемы с пониманием условных переменных и их использования с мьютексами, надеюсь, сообщество поможет мне в этом. Обратите внимание, что я пришел из фона win32, поэтому я использую CRITICAL_SECTION, HANDLE, SetEvent, WaitForMultipleObject и т. Д.

Вот моя первая попытка параллелизма с использованием стандартной библиотеки c ++ 11, это модифицированная версия пример программы можно найти здесь.

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>int _tmain(int argc, _TCHAR* argv[])
{
std::queue<unsigned int>    nNumbers;

std::mutex                  mtxQueue;
std::condition_variable     cvQueue;
bool                        m_bQueueLocked = false;

std::mutex                  mtxQuit;
std::condition_variable     cvQuit;
bool                        m_bQuit = false;std::thread thrQuit(
[&]()
{
using namespace std;

this_thread::sleep_for(chrono::seconds(7));

// set event by setting the bool variable to true
// then notifying via the condition variable
m_bQuit = true;
cvQuit.notify_all();
}
);

std::thread thrProducer(
[&]()
{
using namespace std;

int nNum = 0;
unique_lock<mutex> lock( mtxQuit );

while( ( ! m_bQuit ) &&
( cvQuit.wait_for( lock, chrono::milliseconds(10) ) == cv_status::timeout ) )
{
nNum ++;

unique_lock<mutex> qLock(mtxQueue);
cout << "Produced: " << nNum << "\n";
nNumbers.push( nNum );
}
}
);

std::thread thrConsumer(
[&]()
{
using namespace std;

unique_lock<mutex> lock( mtxQuit );

while( ( ! m_bQuit ) &&
( cvQuit.wait_for( lock, chrono::milliseconds(10) ) == cv_status::timeout ) )
{
unique_lock<mutex> qLock(mtxQueue);
if( nNumbers.size() > 0 )
{
cout << "Consumed: " << nNumbers.front() << "\n";
nNumbers.pop();
}
}
}
);

thrQuit.join();
thrProducer.join();
thrConsumer.join();

return 0;
}

Несколько вопросов по этому поводу.

Я прочитал это msgstr «любой поток, который намерен ждать на std :: condition_variable, должен сначала получить std :: unique_lock.»

Итак, у меня есть {quit mutex, переменная условия & bool}, чтобы указать, когда о выходе было сообщено. Каждый из потоков производителей и потребителей должен получить std :: unique_lock следующим образом:

std::unique_lock<std::mutex> lock(m_mtxQuit);

Это запутывает меня до чертиков. Разве это не заблокирует выходной мьютекс в первом потоке, тем самым блокируя второй? И если это правда, то как первый поток снимает блокировку, чтобы другой поток мог начаться?

Другой вопрос: если я изменю вызов wait_for () на ожидание в течение нулевых секунд, этот поток будет голодать. Может кто-нибудь объяснить? Я ожидаю, что он не будет блокироваться перед выполнением цикла while (правильно ли я предположить, что no_timeout записывается вместо timeout?).

Как я могу вызвать wait_for () и указать нулевое время, чтобы вызов wait_for () не блокировался, а просто проверял условие и продолжал?

Мне также было бы интересно услышать о хороших ссылках на эту тему.

12

Решение

Разве это не заблокирует выходной мьютекс в первом потоке, тем самым блокируя второй?

Да.

И если это правда, то как первый поток снимает блокировку, чтобы другой поток мог начаться?

Когда вы ждете на condition_variable он открывает замок, который вы передаете, так что в

cvQuit.wait_for( lock, chrono::milliseconds(10) )

переменная условия вызовет lock.unlock() и затем блокировка на срок до 10 мс (это происходит атомарно, поэтому нет никакого промежутка между разблокировкой мьютекса и блокировкой, когда условие может быть готово, и вы его пропустите)

Когда мьютекс разблокирован, он позволяет другому потоку получить блокировку на нем.

Другой вопрос: если я изменю вызов wait_for () на ожидание в течение нулевых секунд, этот поток будет голодать. Может кто-нибудь объяснить?

Я бы ожидал Другой поток должен быть истощен, потому что мьютекс не разблокируется достаточно долго, чтобы другой поток мог его заблокировать.

правильно ли я предположить, что no_timeout является recv’d вместо timeout?

Нет, если время истекает без того, чтобы условие стало готовым, то оно «истекает» даже через ноль секунд.

Как я могу вызвать wait_for () и указать нулевое время, чтобы вызов wait_for () не блокировался, а просто проверял условие и продолжал?

Не используйте условную переменную! Если ты не хочешь Подождите чтобы условие стало истинным, не ждите переменную условия! Просто проверить m_bQuit и продолжить.
(Кроме того, почему ваши логические значения называются m_bXxx? Они не члены, поэтому m_ префикс вводит в заблуждение, а b Приставка выглядит как та ужасная привычка MS венгерской нотации … которая воняет.)

Мне также было бы интересно услышать о хороших ссылках на эту тему.

Лучшая ссылка — Энтони Уильямс C ++ параллелизм в действии который подробно охватывает все атомарные и потоковые библиотеки C ++ 11, а также общие принципы многопоточного программирования. Одна из моих любимых книг на эту тему — Butenhof’s Программирование с помощью POSIX Threads, что характерно для Pthreads, но средства C ++ 11 очень тесно связаны с Pthreads, поэтому легко перенести информацию из этой книги в многопоточность C ++ 11.

Нотабене В thrQuit ты пишешь в m_bQuit без защиты мьютексом, поскольку ничто не мешает другому потоку читать его одновременно с записью, это условие гонки, то есть неопределенное поведение. Запись в bool должна быть либо защищена мьютексом, либо должна быть атомарного типа, например std::atomic<bool>

Я не думаю, что вам нужны два мьютекса, это только добавляет раздора. Так как вы никогда не выпускаете mtxQuit кроме как в ожидании condition_variable нет смысла иметь второй мьютекс, mtxQuit один уже гарантирует, что только один поток может войти в критическую секцию одновременно.

11

Другие решения

Если вы хотите что-то проверить и продолжить независимо от того, является ли это истинным или нет (возможно, делать две разные вещи), тогда переменная условия не подходит для использования. Переменная условия — это низкоуровневый примитив для некоторого условия, связанного со структурой заблокированных данных, которую вы хотите ждать без необходимости раскручивать получение и снятие блокировки. Каноническим примером является очередь — у вас есть блокировка, защищающая доступ к очереди, и две переменные условия (очередь не пуста и очередь не заполнена). Чтобы вставить что-то в очередь, вы получаете блокировку, проверяете, что она не заполнена, ждите на незаполненном condvar, если он есть, выдвигаете значение в очереди, сигнализируете о непустом condvar (так как он больше не пуст) и снять замок. Поп-операция похожа.

Так что в вашем случае у вас есть простая очередь, которая не может быть заполнена, поэтому вам нужен один замок и один condvar для нее. Имеет смысл. Но тогда у вас есть флаг «Выход», который вы хотите, чтобы завершить триггер. Вы не хотите ждать, пока будет установлен флаг выхода — вы действительно хотите выполнять работу, пока он не будет установлен — поэтому condvar здесь действительно не имеет смысла. Да, вы МОЖЕТЕ придумать замысловатую договоренность, которая заставит его работать, но это может сбить с толку, так как в нем не используется условная переменная в качестве условной переменной.

Логичнее (и понятнее) просто использовать std::atomic<bool> для флага выхода. Затем вы просто инициализируете значение false, установите значение true в свой поток выхода и проверьте его в других потоках.

1