Mutex. Синхронизация потоков.
Как было наглядно видно в теме 00 – при работе с потоками они могут обращаться к ресурсам в асинхронном режиме, что приводит к некоторым ошибкам (например некорректный вывод данных в консоль) при работе нескольких потоков одновременно.
Для борьбы с этой проблеммой применяют методы синхронизации потоков (некоторая часть “переехала” из чистого C и RTOS).
Наиболее известные способы:
– Мьютексы (Mutex) –по сути это блокировка ресурса на время выполнения задачи.
– Формирование очередей (упорядоченный список на обработку) что также используется для взаимодействия отдельных потоков.
– Бинарные и счетные семафоры – по сути это производная от очередей, где происходят оповещения от одной задачи другой об освобождении ресурсов, т.е. «общий ресурс» говорит задаче/ам что он свободен, а те если у них есть данные «проверяют ресурс на доступность/свободность ресурса».
Бинарные семафоры могут принимать значения 0 и 1 (по сути как очередь длинны 1),
Cчетные семафоры равносильны очереди длинной N
//———————————————————————————————
Рассмотрим «обычный» mutex
Для работы необходимо подключить библиотеку mutex:
#include <mutex>
Напишем следующую программу:
#include <iostream>
#include "thread"
#include "chrono"
#include "string.h"
#include <mutex>
using namespace std;
mutex mtx; //создаем объект класса mutex
void Print5Msg(string msg)
{
mtx.lock();
cout<<"Mx Locked"<<endl;
for (int var = 0; var < 5; ++var)
{
cout<<msg<<endl;
this_thread::sleep_for(chrono::milliseconds(100)); //блокирует/переводит текущий поток в режим ожидания на 100мс
}
cout<<"Mx Unlocked"<<endl<<endl;
mtx.unlock();
}
void Msg1()
{
cout<<"Task ID="<<this_thread::get_id()<<endl;
Print5Msg("MSG1");
}
void Msg2()
{
cout<<"Task ID="<<this_thread::get_id()<<endl; Print5Msg(">>MSG2");
}
int main()
{
thread task1(&Msg1);
thread task2(&Msg2);
task1.join();
task2.join();
return 0;
}
В данном примере строки cout<<«Task ID=»<<this_thread::get_id()<<endl;
в обоих потоках не защищены мьютексами и обычно появляются «артефакты при их вызове» (стоит обратить внимание, что стартуют потоки почти одновременно).
Функция Print5Msg(…); имеет защиту в виде мьютексов и хотя потоки стартовали почти одновременно и имеют одинаковую задержку на вывод (100мс) не происходит наложения и смешивания вывода из обоих потоков.
Стоит также отметить, что в конкретно данном случае теряется смысл от многопоточности, т.к. основное действие происходит в однопоточном режиме в функции Print5Msg.
Когда поток доходит до метода mtx.lock(); он проверяет что ресурс занят, и если да, то ожидает его освобождения.
Если внутри защищенной области использовать код mtx.lock(); (по сути дважды защитить ресурс) – будет ошибка.
Примечание: необходимо не забыть разблокировать mutex!!! Иначе код заблокируется.
//———————————————————————————————
Взаимная блокировка:
Как было показано выше – mutex это объект класса, следовательно можно создавать и использовать несколько mutex внутри функций, но тут есть одна опасность.
Рассмотрим на примере:
Пусть есть 2 мьютекса и 2 функции
mutex mtx1; //создаем объект класса mutex
mutex mtx2; //создаем объект класса mutex
void func1()
{
mtx1.lock();
//code1
mxt2.lock();
//code2
mxt1.unlock();
mtx2.unlock();
}
void func2()
{
mtx2.lock();
//code3
mxt1.lock();
//code4
mxt1.unlock();
mtx2.unlock();
}
Если функции стартуют одновременно – они обе зависнут на code1 и code3 соответственно, т.к. mxt1.lock(); mxt2.lock();
Для борьбы с подобным можно использовать например одинаковый порядок мьютексов в обеих функциях mtx1.lock(); //code1 mxt2.lock();
//———————————————————————————————
Рекурсивный мьютекс:
В рекурсивных функциях может рекурсивно вызываться метод mutex.lock();, но такой код вызовет ошибку, для рекурсивных функций используется рекурсивный мьютекс.
По сути внутри него есть счетчик, который считает число lock и unlock, поэтому ошибки не возникает.
Важно!!! Освобождать мьютекс надо столько же раз сколько и заблокировали!!!
Синтаксис:
recursive_mutex r_mtx;
методы r_mtx.lock(); r_mtx.unlock();
//———————————————————————————————
lock_guard mutex
lock_guard mutex–по сути немного модифицированный mutex который вызывает mutex.lock(); в конструкторе и mutex.unlock(); в деструкторе.
Т.е. lock_guard mutex позволяет не задумываться о разблокировке потока.
Минус lock_guard mutex в том, что он блокирует код мьютексом до вызова деструктора, т.е. его можно применять только в однопоточных частях функций (вплоть до выхода из функции).
Т.е. если код –смесь из однопоточной и многопоточных частей – лучше использовать обычный mutex или unique_lock.
синтаксис:
mutex mtx; //создаем объект класса mutex
void Print5Msg(string msg)
{
lock_guard quard(mtx);
cout<<"Mx Locked"<<endl;
for (int var = 0; var < 5; ++var)
{
cout<<msg<<endl;
this_thread::sleep_for(chrono::milliseconds(100)); //блокирует/переводит текущий поток в режим ожидания на 100мс
}
cout<<"Mx Unlocked"<<endl<<endl;
}
//———————————————————————————————
unique_lock mutex
Для исправления минуса lock_guard mutex используется unique_lock – он также вызывает mutex.lock(); в конструкторе и mutex.unlock(); в деструкторе ИЛИ В СПЕЦИАЛЬНОМ МЕТОДЕ!
Т.е. если вручную не разблокировать мьютекс – он разблокируется в деструкторе.
mutex mtx; //создаем объект класса mutex
void Print5Msg(string msg)
{
unique_lock u_lock(mtx);
cout<<"Mx Locked"<<endl;
for (int var = 0; var < 5; ++var)
{
cout<<msg<<endl;
this_thread::sleep_for(chrono::milliseconds(100)); //блокирует/переводит текущий поток в режим ожидания на 100мс
}
cout<<"Mx Unlocked"<<endl<<endl;
u_lock.unlock();
//Multithread code
}
Есть некоторые интересные методы:
unique_lock u_lock(mtx,std::defer_lock); //просто создаем объект, не вызывая mtx.lock();
u_lock.lock(); // вызываем mtx.lock();