|
|||||||
Как работает мьютекс (mutex)? Простейший пример использования мьютекса в C++
Время создания: 17.12.2024 10:54
Автор: Xintrea
Текстовые метки: c++, си++, мутекс, мьютекс, mutex, блокировка, защита, синхронизация, ресурс, спин-лок, спинлок, spin-lock, spinlock, многопоточность, поток, процесс, параллельное, программирование
Раздел: Компьютер - Программирование - Язык C++ (Си++)
Запись: xintrea/mytetra_syncro/master/base/1734422075c12ugstkaz/text.html на raw.github.com
|
|||||||
|
|||||||
В C++ мьютекс (mutex, от mutual exclusion) используется для обеспечения синхронизации потоков и предотвращения одновременного доступа к общим данным из разных потоков. Он позволяет одному потоку захватить ресурс и блокировать доступ к нему для других потоков, пока работа с ресурсом не завершится. Что такое "ресурс", хорошо написано в этой статье: Как работает спин-лок? Пример простейшего спин-лока на C++. Основные этапы работы мьютекса:
Поддержка мьютексов в C++ Standard Library C++ предоставляет мьютексы в заголовке <mutex>. Основные варианты мьютексов следующие:
Пример использования Ниже приведен простейший пример использования std::mutex. #include <iostream> #include <thread> #include <mutex> // Глобальный мьютекс std::mutex mtx; // Общий ресурс int sharedCounter = 0; // Функция для потоков, увеличивающая счетчик void incrementCounter(int iterations, const std::string& threadName) { for (int i = 0; i < iterations; ++i) { // Мьютекс захватывается перед доступом к общему ресурсу mtx.lock(); // Критическая секция ++sharedCounter; std::cout << threadName << " counter " << sharedCounter << std::endl; mtx.unlock(); // Мьютекс освобождается } } int main() { const int iterations = 5; // Создаем два потока std::thread t1(incrementCounter, iterations, "Thread 1"); std::thread t2(incrementCounter, iterations, "Thread 2"); // Дожидаемся завершения потоков t1.join(); t2.join(); std::cout << "Final value: " << sharedCounter << std::endl; return 0; } Вывод этого кода будет выглядеть так: Thread 1 counter 1 Thread 2 counter 2 Thread 1 counter 3 Thread 2 counter 4 Thread 1 counter 5 Thread 2 counter 6 Thread 1 counter 7 Thread 2 counter 8 Thread 1 counter 9 Thread 2 counter 10 Final value: 10 В этом коде мьютекс mtx используется для защищиты доступа к критической секции, в которой происходит изменение глобальной переменной sharedCounter, и вывод на консоль ее нового значения. При работе примера нужно обратить внимание, что каждый поток выполняет 5 итераций, всего 2 потока. Увеличение переменной на единицу происходит в разных потоках, но ничего не путается, и переменная увеличивается ровно на единицу в любой итерации любого потока. Это говорит о том, что начальное значение переменной при каждой итерации успешно атомарно изменено и уложено в память на предыдущей итерации, независимо от того, в каком потоке это производилось. Важно для понимания В вышеприведенном примере мьютекс не просто так объявлен в глобальной области видимости. Это сделано для того, чтобы продемонстрировать, что блокировка должна происходить, опираясь на один и тот же объект мьютекса, который один для всех потоков. Если объявить мьютекс внутри функции incrementCounter(), которая выполняется параллельно в двух потоках, то в параллель бы создавалось два экземпляра мьютекса, которые никак не связаны друг с другом, и никакой синхронизации потоков в этом случае не происходило бы. Об этом обязательно надо помнить и следить за тем, где расположен сам объект мьютекса, чтобы он обеспечивал синхронизацию. Кроме того, из простых примеров кода может показаться, что один мьютекс используется только в одном месте параллельного кода. Но это не обязательно так. Вполне может существовать код, который по различным причинам может использовать один и тот же мьютекс в разных параллельных частях программы. Использование вспомогательных классов при работе с мьютексами В примере выше видно, что для избежания бесконечного ожидания разблокировки мьютекса, необходимо чтобы каждая блокировка сопровождалась разблокировкой. В простом примере за этим легко уследить, но когда код становится сложным, разветвленным, с досрочным выходом из функции, возможны ошибки программиста, когда разблокировка не произошла. Причем это состояние может возникнуть именно при определенных условиях, которые происходят редко, например при определенной комбинации исходных данных. И тогда поиск первопричины проблемы превращается в трудно решаемую задачу. Чтобы обеспечить гарантированную разблокировку мьютекса, можно использовать вспомогательный класс, который инициализируется ссылкой на мьютекс, и у которого в конструкторе прописана блокировка мьютекса, а в деструкторе стоит команда разблокировки. Такой объект при выходе из области видимости будет разрушаться, что автоматически вызовет разблокировку мьютекса. В C++ имеется класс std::lock_guard. При создании объекта он автоматически в конструкторе вызывает метод lock() переданного ему мьютекса, а в деструкторе и вызывает метод unlock() для мьютекса. Использование класса std::lock_guard показано ниже. for (int i = 0; i < iterations; ++i) { // Мьютекс захватывается перед доступом к общему ресурсу // Захват происходит через класс std::lock_guard std::lock_guard<std::mutex> locker(mtx); // Критическая секция ++sharedCounter; std::cout << threadName << " counter " << sharedCounter << std::endl; } В этом коде разблокировка мьютекса происходит неявно в конце каждой итерации цикла, так как объект locker выходит из области видимости в конце итерации, и у него срабатывает деструктор, который вызывает unlock() для мьютекса mtx. Кстати, следует напомнить, что синтаксис "... locker(mtx);" - это синтаксис инициализации создаваемого объекта. В данном случае объект locker инициализируется передаваемой ему ссылкой на объект mtx. Любители C++ часто любят зашифровать код и смешать понятия, поэтому в примерах в интернете можно встретить такую команду: std::lock_guard<std::mutex> lock(mtx); Здесь написан не хитроумный вызов метода lock() неизвестного временного объекта с передачей ему ссылки на объект mtx. Здесь просто создается объект типа std::lock_guard<std::mutex> с именем lock, который инициализируется ссылкой на объект mtx. Как устроено ожидание разблокировки внутри мьютекса? Реализация мьютекса обычно комбинирует активное ожидание (spin-lock) и блокировку потока. В случае многозадачной ОС, как правило, вначале на короткое время включается spin-lock, а в случае, если время ожидания становится большим, происходит блокировка потока: поток помещается в очередь ожидания (внутри ядра ОС), освобождая процессорные ресурсы. Когда поток "засыпает" (suspended), ОС освобождает процессорные ресурсы для других потоков. Как только мьютекс освобождается, ОС "будит" один из потоков, ожидающих мьютекс. Другими словами:
Таким образом, не само ядро управляет потоками с мьютексом, а сам мьютекс при определенных условиях передает управление ядру, чтобы поток, в котором он находится, перешел в состояние suspended. Итого Мьютексы эффективно синхронизируют потоки, предотвращая различные проблемы синхронизации потоков (например, состояние гонки). Важно грамотно их использовать мьютексы. Неправильное их использование может привести к взаимным блокировкам (deadlock) или высокой задержке выполнения кода программы. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|