std::mutex и как его все таки нужно использовать для разных участков
Я все таки окончательно запутался в мьютексах и как их нужно правильно применять к синхронизации разных участков кода:
Вот два простых примера: различаются только доп. наличием переменной std::my_mutex1
1)
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex my_mutex;
std::condition_variable my_cond_var;
std::list<int>my_list;
int global_int;
void waiting_threads(int id)
{
repeat:
std::cout << "waiting_threads_start" << std::endl;
//----------------------------------------------------------------
{
std::unique_lock<std::mutex> my_lock(my_mutex);
while (global_int == 0)
{
my_cond_var.wait(my_lock);
}
std::cout << "thread_id:" << std::this_thread::get_id() << ":" << id << std::endl;
}
//----------------------------------------------------------------
std::this_thread::sleep_for(std::chrono::seconds(3));
my_mutex.lock();
global_int = 0;
my_mutex.unlock();
goto repeat;
}
int main()
{
global_int = 0;
//------------------------------------------------------------------------Создадим пул потоков:Начало-----------------------------
std::vector<std::thread> my_thread_vec;
my_thread_vec.resize(4);
for (int i = 0; i < 4; i++)
{
my_thread_vec[i] = std::thread(waiting_threads, i);
}
//------------------------------------------------------------------------Создадим пул потоков:Конец--------------------
std::this_thread::sleep_for(std::chrono::seconds(1));
my_mutex.lock();
std::cout << "main_start" << std::endl;
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
std::cout << "sleep_main_start" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "sleep_main_end" << std::endl;
global_int = 1;
my_cond_var.notify_one();
std::cout << "main_mutex_end" << std::endl;
my_mutex.unlock();
//------------------------------------------------Отпускаем потоки, так как у них бесконечный цикл---------------------------------------
for (int i = 0; i < 4; i++)
{
my_thread_vec[i].join();
}
//---------------------------------------------------------------------------------------------------------------------------------------
}
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex my_mutex;
std::mutex my_mutex1;
std::condition_variable my_cond_var;
std::list<int>my_list;
int global_int;
void waiting_threads(int id)
{
repeat:
std::cout << "waiting_threads_start" << std::endl;
//----------------------------------------------------------------
{
std::unique_lock<std::mutex> my_lock(my_mutex);
while (global_int == 0)
{
my_cond_var.wait(my_lock);
}
std::cout << "thread_id:" << std::this_thread::get_id() << ":" << id << std::endl;
}
//----------------------------------------------------------------
std::this_thread::sleep_for(std::chrono::seconds(3));
my_mutex.lock();
global_int = 0;
my_mutex.unlock();
goto repeat;
}
int main()
{
global_int = 0;
//------------------------------------------------------------------------Создадим пул потоков:Начало-----------------------------
std::vector<std::thread> my_thread_vec;
my_thread_vec.resize(4);
for (int i = 0; i < 4; i++)
{
my_thread_vec[i] = std::thread(waiting_threads, i);
}
//------------------------------------------------------------------------Создадим пул потоков:Конец--------------------
std::this_thread::sleep_for(std::chrono::seconds(1));
my_mutex1.lock();
std::cout << "main_start" << std::endl;
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
global_int = 1;
my_cond_var.notify_one();
std::cout << "sleep_main_start" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "sleep_main_end" << std::endl;
global_int = 1;
my_cond_var.notify_one();
std::cout << "main_mutex_end" << std::endl;
my_mutex1.unlock();
//------------------------------------------------Отпускаем потоки, так как у них бесконечный цикл---------------------------------------
for (int i = 0; i < 4; i++)
{
my_thread_vec[i].join();
}
//---------------------------------------------------------------------------------------------------------------------------------------
}
В первом случае изменение значения global_int заблокированы my_mutex, во втором my_mutex1. Из за этого у код разное поведение.
-В первом случае: потоки пробуждаются только после main_mutex_end - пробуждаются каждый по одному разу и почему то 5ый раз пробуждение не срабатывает.
-Во втором случае: потоки пробуждаются сразу после вызова notify_one() и во втором случае пробуждается один и з потоков и 5ый раз.
Не могу сообразить, что происходит:
-В первом случае создаются 4 потока и в них запускается функция waiting_threads, поток сразу же захватывает my_mutex и так как переменная global_int равна 0, то поток засыпает блокируя остальные потоки. Потом выполнение кода доходит до строки с захватам того же мьютекса в main() и вот тут не понятно, что происходит.
-В втором случае создаются 4 потока и в них запускается функция waiting_threads, поток сразу же захватывает my_mutex и так как переменная global_int равна 0, то поток засыпает блокируя остальные потоки. Потом выполнение кода доходит до строки с захватам my_mutex1 в main()(другого нежели в ф-ии waiting_threads) и вот тут тоже, что то не понятно, что происходит. Вроде бы участок с кода в котором присутствует обращение к переменной global_int уже под мьютексом my_mutex, а тут захватывается еще один мьютекс my_mutex1 и в участке кода тоже есть обращение к переменной global_int.
Я в конец запутался.
Ответы (1 шт):
Вы не понимаете принцип работы std::condition_variable. Давайте начнем с понимания, что делает метод std::condition_variable::wait(). А делает он две вещи:
атомарно переводит поток в режим ожидания события и разблокирует мьютекс. Причем то, что это происходит атомарно здесь ключевое, и именно из-за этого
std::condition_variableи существует.по наступлении события (другой поток вызвал
std::condition_variable::notify_one()илиstd::condition_variable::notify_all()поток переводится из режима ожидания в режим выполнения, и он пытается заблокировть мьютекс и затем продолжить выполнение. Поток может быть также разбужен системными событиями (spurious wakeup).
То есть, во время ожидания события мьютекс разблокирован, но чтобы вернутся из ожидания и продолжить выполнение кода, поток должен быть как разбужен событием (или может быть разбужен например сигналом (spurious wakeup), поэтому условие в ожидании проверяется в цикле) но и опять захватить мьютекс.
Далее, поток, который должен сообщить одному или более ожидающим потокам, что событие наступило делает следующие вещи:
- блокирует мьютекс
- устанавливает состояние, которое ожидают
- посылает сообщение notify_one() или notify_all()
- разблокирует мьютекс
Причем шаг 3 и шаг 4 можно менять местами.
Что же делает вызов метода std::condition_variable::notify_one()? если в данный момент на данной переменной один из потоков ожидает события, то он выводится из режима ожидания. Если таких потоков нет то вызов std::condition_variable::notify_one() игнорируется и не запоминается
При этом разница между std::condition_variable::notify_one() и std::condition_variable::notify_all() только в том, что первый метод переведет из режима ожидания только один поток, даже если ожидают многие, а второй их все. Но и в том и в другом случае поток уже в данный момент должен находится в режиме ожидания, чтобы этот сигнал получить.
Если вы поймете принцип работы, то вам станет понятен ответ на ваш вопрос
-В первом случае: потоки пробуждаются только после main_mutex_end - пробуждаются каждый по одному разу и почему то 5ый раз пробуждение не срабатывает.
Потому что вначале на переменной ожидают 4 потока. Вы их все переводите в режим выполнения, но выполниться им не даете, потому что не разблокировали мьютекс. Пятый же вызов проходит вхолостую - на этой переменной события больше никто не ждет.
Кроме того, ваша функция ожидания некорректна, вы не должны сбрасывать переменную в ноль, да еще и после отдельного захвата мьютекса, должна она выглядеть примерно вот так:
void waiting_threads(int id)
{
std::cout << "waiting_threads_start" << std::endl;
while( true ) {
{
//-----------------------------------
std::unique_lock<std::mutex> my_lock(my_mutex);
while (global_int == 0)
{
my_cond_var.wait(my_lock);
}
--global_int;
std::cout << "thread_id:" << std::this_thread::get_id() << ":" << id << std::endl;
//-------------------------------------------------------
}
std::this_thread::sleep_for(std::chrono::seconds(3));
}
}
Здесь не хватает логики завершения работы, но мы ее опустим для простоты.
Далее, если вы посмотрите на участок кода, между комментарием //-----
то заметите, что выполняется он под захваченным мьютексом. Это значит, что выполняется в один момент только один поток. То есть, когда вы находитесь здесь, главный поток либо уже установил переменную global_int в ненулевое значение и цикл ожидания не будет выполнятся, либо еше нет, но тогда вы не пропустите сигнала на std::condition_variable поскольку послать сигнал основной поток сможет только тогда, когды вы отпустите мьютекс и находитесь в режиме ожидания.
Следует заметить, что событие, которое вы ожидаете, это не сигнал на std::condition_vazriable, а состояние, когда global_int не равно нулю. С данной логикой событие вы не пропустите, вне зависимости от того, получили вы сигнал от std::condition_variable или нет.
Кроме того, основная функция не должна устанавливать global_int в значение 1, а должна увеличивать ее на 1. Вы должны понять, что событие завязано на переменную global_int и она в данном случае определяет, какое количество работы стоит в очереди. std::condition_variable здесь вспомогательное средство, и вы не должны строить свою логику на количестве вызовов notify_one или notify_all - это лишь средство сообщить ожидающим потокам, что состояние изменилось.