Что такое windows mutex

[C++] часть 2: МЬЮТЕКС. Пишем наш первый код для многопоточной среды

Научитесь создавать код для использования в многопоточной среде с помощью реализации потокобезопасной очереди

Nov 30, 2019 · 8 min read

В прошлой статье мы разобрались с тем, что такое конкурентность/параллелизм и зачем нужна синхронизация. Настала пора изучить примитивы синхронизации, которые предлагает нам стандартная библиотека шаблонов C++.

Первым из них будет std::mutex . Но сначала ознакомьтесь с картой статьи (она пригодится, если вы вдруг запутаетесь).

Что такое мьютекс?

Мьютекс (англ. mutex, от mut ual ex clusion — «взаимное исключение») — это базовый механизм синхронизации. Он предназначен для организации взаимоисключающего доступа к общим данным для нескольких потоков с использованием барьеров памяти (для простоты можно считать мьютекс дверью, ведущей к общим данным).

Синтаксис

  • Заголовочный файл | #include
  • Объявление | std::mutex mutex_name;
  • Захват мьютекса | mutex_name .lock();
    Поток запрашивает монопольное использование общих данных, защищаемых мьютексом. Дальше два варианта развития событий: происходит захват мьютекса этим потоком (и в этом случае ни один другой поток не сможет получить доступ к этим данным) или поток блокируется (если мьютекс уже захвачен другим потоком).
  • Освобождение мьютекса | mutex_name .unlock();
    Когда ресурс больше не нужен, текущий владелец должен вызвать функцию разблокирования unlock, чтобы и другие потоки могли получить доступ к этому ресурсу. Когда мьютекс освобождается, доступ предоставляется одному из ожидающих потоков.

Как создать потокобезопасную очередь

Разберёмся, как реализовать простейшие потокобезопасные очереди, то есть очереди с безопасным доступом для потоков.
В библиотеке стандартных шаблонов уже есть готовая очередь ( rawQueue ). Наша реализация будет предполагать: а) извлечение и удаление целочисленного значения из начала очереди и б) добавление нового в конец очереди. И всё это при обеспечении потокобезопасности.

Сначала выясним, почему и как эти две операции могут создавать проблемы для многопоточности.

  • Извлечение и удаление
    Для извлечения и удаления значения из начала очереди необходимо выполнить три операции:
    1. Проверить, не пуста ли очередь.
    2. Если нет, получается ссылка на начало очереди ( rawQueue .front() ).
    3. Удаляется начало очереди ( rawQueue .pop() ).
    В промежутках между этими этапами к очереди могут получать доступ и другие потоки с целью внесения изменений или чтения. Попробуйте сами.

Получается, мы должны быть уверены, что никто не будет трогать очередь, пока мы выполняем наши задачи. Используем мьютекс для защиты этих многоступенчатых операций и сделаем так, чтобы все вместе они смотрелись как одна атомарная операция.

Обратите внимание:

  1. Связь между мьютексом и защищаемым ресурсом — только в голове программиста.
    Мы знаем, что мьютекс m защищает rawQueue , но напрямую это не указывается.
  2. Захват с необходимой степенью распараллеливания.
    Использование мьютекса уменьшает параллелизм. Предположим, у нас только один мьютекс для защиты вектора и строки без каких-либо зависимостей (например, значение переменной не зависит от вектора и наоборот). Поток А захватывает мьютекс, читает строку и начинает обработку каких-то данных перед добавлением нового значения в вектор и освобождением мьютекса. И тут появляется поток B, который хочет внести пару изменений в строку, но при попытке захватить мьютекс (какая досада!) оказывается блокированным до того момента, пока не будут завершены все операции с вектором. Проблему решаем дополнительным мьютексом для строки ( захваченным перед чтением и освобождённым сразу по завершении).
    → Всегда прикидывайте, какой объём данных будет защищён одним мьютексом.
  3. Проводите захват только для тех операций, которым это необходимо.
    См. предыдущий пункт.
  4. Не вызывайте lock() , если мьютекс у вас уже есть.
    Мьютекс уже заблокирован, и попытки повторного захвата приведут вас к состоянию бесконечного ожидания. Если он вам так нужен, можно воспользоваться классом std::recursive_mutex . Рекурсивный мьютекс можно получить одним и тем же потоком много раз, но столько же раз он должен быть и освобождён.
  5. Используйте try_lock() или std::timed_mutex , если не хотите блокироваться и ожидать неопределённое время.
    → try_lock() — это неблокирующий метод в std::mutex . Он возвращает немедленно, даже если владение не получено, причём со значением true, если мьютекс захвачен, и false — если нет.
    → std::timed_mutex предлагает два неблокирующих метода для захвата мьютекса: try_lock_for() и try_lock_until() , причём оба возвращаются по истечении времени со значением true или false в зависимости от успешности захвата.
  6. Не забывайте вызывать unlock() или используйте std::lock_guard (или другие шаблонные классы), когда есть возможность.
    См. ниже.
Читайте также:  Как отключить живые плитки windows phone 10

Lock guard и парадигма RAII

У нас две большие проблемы с этим простым мьютексом:

  • Что произойдёт, если мы забудем вызвать unlock() ? Ресурс будет недоступен в течение всего времени существования мьютекса, и уничтожение последнего в неразблокированном состоянии приведёт к неопределённому поведению.
  • Что произойдёт, если до вызова unlock() будет выброшено исключение? unlock() так и не будет исполнен, а у нас будут все перечисленные выше проблемы.

К счастью, проблемы можно решить с помощью класса std::lock_guard . Он всегда гарантированно освобождает мьютекс, используя парадигму RAII (Resource Acquisition Is Initialization, что означает «получение ресурса есть инициализация»). Вот как это происходит: мьютекс инкапсулируется внутри lock_guard, который вызывает lock() в его конструкторе и unlock() в деструкторе при выходе из области видимости. Это безопасно даже при исключениях: раскрутка стека уничтожит lock_guard , вызывая деструктор и таким образом освобождая мьютекс.

Посмотрим теперь, как можно изменить нашу потокобезопасную очередь threadSafe_queue (на этот раз обращаем внимание на то, где освобождается мьютекс).

Unique lock, дающий свободу

Как только владение мьютексом получено (благодаря std::lock_guard ), он может быть освобождён. std::unique_lock действует в схожей манере плюс делает возможным многократный захват и освобождение (всегда в таком порядке) мьютекса, используя те же преимущества безопасности парадигмы RAII.

Когда использовать?

  • Когда вам не всегда нужен захват ресурса.
  • Вместе с std::condition_variable (в следующей статье).
  • При захвате std::shared_mutex в эксклюзивном режиме (см. далее).

Общий мьютекс + общий захват дают больше читателей

std::mutex — это мьютекс, которым одномоментно может владеть только один поток. Однако это ограничение не всегда обязательно. Например, потоки могут одновременно и безопасно читать одни и те же общие данные. Просто читать, не производя с ними никаких изменений. Но в случае с доступом к записи только записывающий поток может иметь доступ к данным.

Начиная с C++17, std::shared_mutex формирует доступ двух типов:

  • Общий доступ: потоки вместе могут владеть мьютексом и иметь доступ к одному и тому же ресурсу. Доступ такого типа можно запросить с помощью std::shared_lock (lock guard для общего мьютекса). При таком доступе любой эксклюзивный доступ блокируется.
  • Эксклюзивный доступ: доступ к ресурсу есть только у одного потока. Запрос этого типа осуществляется с помощью класса unique lock.

Синтаксис

  • Заголовочный файл | #include ;
  • Объявление | std::shared_mutex raw_sharedMutex;
  • Для захвата в общем режиме |
    std::shared_lock > sharedLock_name(raw_sharedMutex);
  • Для захвата в эксклюзивном режиме |
    std::unique_lock > uniqueLock_name(raw_sharedMutex);

Scoped lock, дающий больше мьютексов (и без клинча)

Впервые появившийся в C++17 и действующий в схожей манере, что и std::lock_guard , он даёт возможность получения нескольких мьютексов. Без std::scoped_lock такая операция очень опасна, так как может привести к взаимной блокировке.

Читайте также:  Как установить windows для lenovo

Краткая история взаимоблокировки:

Поток A хочет увести 200$ с банковского счёта Жеки на счёт Бяки в виде одной атомарной операции. Он начинает с того, что захватывает мьютекс, защищающий счёт Жеки, чтобы изъять деньги, а затем пытается захватить счёт Бяки.
В то же время поток B хочет увести 100$ со счёта Бяки на счёт Жеки. Он получает захват счёта Бяки, чтобы изъять деньги и попытаться захватить счёт Жеки. Оба потока блокируются, уснув в ожидании друг друга.

std::scoped_lock одновременно захватывают (а затем освобождают) все мьютексы, передаваемые в качестве аргумента, по принципу « всё или ничего»: если хотя бы один захват выбрасывает исключение, освобождаются все уже захваченные мьютексы.

  • std::scoped_lock scoped_lock_name(raw_mutex1, raw_mutex2, ..);

Заключение

Если вы вдруг запутались в этом ворохе новой информации:

  • воспользуйтесь картой в начале статьи (или составьте свою);
  • применяйте на практике новые знания и пробуйте писать простенький код.

До встречи в следующей статье, в которой речь пойдёт о condition_variable и вы узнаете, как синхронизировать потоки!

Using Mutex Objects

You can use a mutex object to protect a shared resource from simultaneous access by multiple threads or processes. Each thread must wait for ownership of the mutex before it can execute the code that accesses the shared resource. For example, if several threads share access to a database, the threads can use a mutex object to permit only one thread at a time to write to the database.

The following example uses the CreateMutex function to create a mutex object and the CreateThread function to create worker threads.

When a thread of this process writes to the database, it first requests ownership of the mutex using the WaitForSingleObject function. If the thread obtains ownership of the mutex, it writes to the database and then releases its ownership of the mutex using the ReleaseMutex function.

This example uses structured exception handling to ensure that the thread properly releases the mutex object. The __finally block of code is executed no matter how the __try block terminates (unless the __try block includes a call to the TerminateThread function). This prevents the mutex object from being abandoned inadvertently.

If a mutex is abandoned, the thread that owned the mutex did not properly release it before terminating. In this case, the status of the shared resource is indeterminate, and continuing to use the mutex can obscure a potentially serious error. Some applications might attempt to restore the resource to a consistent state; this example simply returns an error and stops using the mutex. For more information, see Mutex Objects.

Мьютексы

Объект взаимного исключения (mutual exception), или мьютекс (mutex), обеспечивает более универсальную функциональность по сравнению с объектом CRITICAL_SECTION. Поскольку мьютексы могут иметь имена и дескрипторы, их можно использовать также для синхронизации потоков, принадлежащих различным процессам. Так, два процесса, разделяющие общую память посредством отображения файлов, могут использовать мьютексы для синхронизации доступа к разделяемым областям памяти.

Объекты мьютексов аналогичны объектам CS, однако, дополнительно к возможности их совместного использования различными процессами, они допускают конечные периоды ожидания, а мьютексы, покинутые (abandoned) завершающимся процессом, переходят в сигнальное состояние.[29] Поток приобретает права владения мьютексом (или блокирует (block) мьютекс) путем вызова функции ожидания (WaitForSingleObject или WaitForMultipleObjects) по отношению к дескриптору мьютекса и уступает эти права посредством вызова функции ReleaseMutex.

Как всегда, необходимо тщательно следить за тем, чтобы потоки своевременно освобождали ресурсы, в которых они больше не нуждаются. Поток может завладевать одним и тем же ресурсом несколько раз, и при этом не будет блокироваться даже в тех случаях, когда уже владеет данным ресурсом. В конечном счете, поток должен освободить мьютекс столько раз, сколько она его захватывала. Такая возможность рекурсивного захвата ресурсов, существующая и в случае объектов CS, может оказаться полезной для ограничения доступа к рекурсивным функциям, а также в приложениях, реализующих вложенные транзакции (nested transactions).

Читайте также:  Как производится настройка обновлений windows

При работе с мьютексами мы будем пользоваться функциями CreateMutex, ReleaseMutex и OpenMutex.

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsa, BOOL bInitialOwner, LPCTSTR lpMutexName)

BOOL ReleaseMutex(HANDLE hMutex)

bInitialOwner — если значение этого флага установлено равным True, вызывающий поток немедленно приобретает права владения новым мьютексом. Эта атомарная операция позволяет предотвратить приобретение прав владения мьютексом другими потоками, прежде чем это сделает поток, создающий мьютекс. Как следует из самого его названия (initial owner — исходный владелец), этот флаг не оказывает никакого действия, если мьютекс уже существует.

lpMutexName — указатель на строку, содержащую имя мьютекса; в отличие от файлов имена мьютексов чувствительны к регистру. Если этот параметр равен NULL, то мьютекс создается без имени. События, мьютексы, семафоры, отображения файлов и другие объекты ядра, упоминаемые в данной книге, — все они используют одно и то же пространство имен, отличное от пространства имен файловой системы. Поэтому имена всех объектов синхронизации должны быть различными. Длина указанных имен не может превышать 260 символов.

Возвращаемое значение имеет тип HANDLE; значение NULL указывает на неудачное завершение функции.

Функция OpenMutex открывает существующий именованный мьютекс. Впоследствии эта функция не обсуждается, но используется в некоторых примерах. Эта функция дает возможность потокам, принадлежащим различным процессам, синхронизироваться так, как если бы они принадлежали одному и тому же процессу. Вызову функции OpenMutex в одном процессе должен предшествовать вызов функции CreateMutex в другом процессе. Для семафоров и событий, как и для отображенных файлов (глава 5), также имеются соответствующие функции Create и Open. При вызове этих функций всегда предполагается, что сначала один процесс, например сервер, вызывает функцию Create для создания именованного объекта, а затем другие процессы вызывают функцию Open, которая завершается неудачей, если именованный объект к этому моменту еще не был создан. Возможен и такой вариант, когда все процессы самостоятельно используют вызов функции Create с одним и тем же именем, если порядок создания объектов не имеет значения.

Функция ReleaseMutex освобождает мьютекс, которым владеет вызывающий поток. Если мьютекс не принадлежит потоку, функция завершается с ошибкой.

BOOL ReleaseMutex(HANDLE hMutex)

Спецификация POSIX Pthreads поддерживает мьютексы. Имеются следующие основные функции:

Функция pthread_mutex_lock является блокирующей и поэтому эквивалентна функции WaitForSingleObject в случае ее применения к дескриптору мьютекса. Функция pthread_mutex_trylock осуществляет опрос и не является блокирующей, соответствуя функции WaitForSingleObject в случае ее применения с нулевым значением интервала ожидания. Потоки Pthreads не поддерживают конечные интервалы ожидания и не предлагают средств, аналогичных Windows-объектам CRITICAL_SECTION.

Читайте также

Покинутые мьютексы

Покинутые мьютексы Мьютекс, владевший которым поток завершился, не освободив его, называют покинутым (abandoned), и его дескриптор переходит в сигнальное состояние. На то, что сигнализирующий дескриптор (дескрипторы) представляет покинутый мьютекс (мьютексы), указывает

Мьютексы, критические участки кода и взаимоблокировки

Мьютексы, критические участки кода и взаимоблокировки Несмотря на то что объекты CS и мьютексы обеспечивают решение задач, подобных той, которая иллюстрируется на рис. 8.1, при их использовании следует соблюдать осторожность, иначе можно создать ситуацию взаимоблокировки

Сравнительный обзор: мьютексы и объекты CRITICAL_SECTION

Сравнительный обзор: мьютексы и объекты CRITICAL_SECTION Как уже неоднократно упоминалось, мьютексы и объекты CRITICAL_SECTION весьма напоминают друг друга и предназначены для решения одного и того же круга задач. В частности, объекты обоих типов могут принадлежать только одного

Оцените статью