Что такое рекурсивный мьютекс
Когда использовать рекурсивный мьютекс?
Я понимаю, что рекурсивный мьютекс позволяет блокировать мьютекс более одного раза, не попадая в тупик, и должен быть разблокирован столько же раз. Но в каких конкретных ситуациях вам нужно использовать рекурсивный мьютекс? Я ищу ситуации на уровне дизайна / кода.
5 ответов
например, когда у вас есть функция, которая вызывает рекурсивно, и вы хотите, чтобы синхронизировать доступ к нему:
без рекурсивного мьютекса вам придется сначала создать функцию «точка входа», и это становится громоздким, когда у вас есть набор функций, которые являются взаимно рекурсивными. Без рекурсивного мьютекса:
рекурсивные и нерекурсивные мьютексы имеют разные варианты использования. Ни один тип мьютекса не может легко заменить другой. Нерекурсивные мьютексы имеют меньше накладных расходов, а рекурсивные мьютексы имеют в некоторых ситуациях полезную или даже необходимую семантику, а в других ситуациях опасную или даже сломанную семантику. В большинстве случаев кто-то может заменить любую стратегию с использованием рекурсивных мьютексов другой более безопасной и эффективной стратегией, основанной на использовании нерекурсивных взаимное исключение.
я столкнулся с необходимостью рекурсивного мьютекса сегодня, и я думаю, что это, возможно, самый простой пример среди опубликованных ответов до сих пор: Это класс, который предоставляет две функции API, Process(. ) и reset ().
обе функции не должны выполняться одновременно, потому что они изменяют внутренние элементы класса, поэтому я хотел использовать мьютекс. Проблема в том, что Process() вызывает reset () внутренне, и это создаст взаимоблокировку, потому что mMutex уже приобретен. Блокировка их рекурсивным блокировка вместо этого устраняет проблему.
Если вы хотите увидеть пример кода, который использует рекурсивные мьютексы, посмотрите на источники «электрического забора» для Linux/Unix. Это был один из распространенных инструментов Unix для поиска «проверки границ» перерасхода чтения/записи и перерасхода, а также использования освобожденной памяти до отчет пришли вместе.
просто скомпилируйте и свяжите электрический забор с источниками (опция-g с gcc / g++), а затем свяжите его с вашим программным обеспечением с опцией link-lefence и начните переход через звонки в malloc / free. http://elinux.org/Electric_Fence
Это, безусловно, будет проблемой, если поток заблокирован, пытаясь получить (снова) мьютекс, которым он уже владел.
есть ли причина не разрешать мьютекс приобретаться несколько раз одним и тем же потоком?
[C++] часть 2: МЬЮТЕКС. Пишем наш первый код для многопоточной среды
В прошлой статье мы разобрались с тем, что такое конкурентность/параллелизм и зачем нужна синхронизация. Настала пора изучить примитивы синхронизации, которые предлагает нам стандартная библиотека шаблонов C++.
Что такое мьютекс?
Мьютекс (англ. mutex, от mutual exclusion — «взаимное исключение») — это базовый механизм синхронизации. Он предназначен для организации взаимоисключающего доступа к общим данным для нескольких потоков с использованием барьеров памяти (для простоты можно считать мьютекс дверью, ведущей к общим данным).
Синтаксис
Как создать потокобезопасную очередь
Разберёмся, как реализовать простейшие потокобезопасные очереди, то есть очереди с безопасным доступом для потоков.
В библиотеке стандартных шаблонов уже есть готовая очередь ( rawQueue ). Наша реализация будет предполагать: а) извлечение и удаление целочисленного значения из начала очереди и б) добавление нового в конец очереди. И всё это при обеспечении потокобезопасности.
Сначала выясним, почему и как эти две операции могут создавать проблемы для многопоточности.
Получается, мы должны быть уверены, что никто не будет трогать очередь, пока мы выполняем наши задачи. Используем мьютекс для защиты этих многоступенчатых операций и сделаем так, чтобы все вместе они смотрелись как одна атомарная операция.
Обратите внимание:
Lock guard и парадигма RAII
У нас две большие проблемы с этим простым мьютексом:
Посмотрим теперь, как можно изменить нашу потокобезопасную очередь threadSafe_queue (на этот раз обращаем внимание на то, где освобождается мьютекс).
Unique lock, дающий свободу
Как только владение мьютексом получено (благодаря std::lock_guard ), он может быть освобождён. std::unique_lock действует в схожей манере плюс делает возможным многократный захват и освобождение (всегда в таком порядке) мьютекса, используя те же преимущества безопасности парадигмы RAII.
Когда использовать?
Общий мьютекс + общий захват дают больше читателей
std::mutex — это мьютекс, которым одномоментно может владеть только один поток. Однако это ограничение не всегда обязательно. Например, потоки могут одновременно и безопасно читать одни и те же общие данные. Просто читать, не производя с ними никаких изменений. Но в случае с доступом к записи только записывающий поток может иметь доступ к данным.
Начиная с C++17, std::shared_mutex формирует доступ двух типов:
Синтаксис
Scoped lock, дающий больше мьютексов (и без клинча)
Краткая история взаимоблокировки:
Поток A хочет увести 200$ с банковского счёта Жеки на счёт Бяки в виде одной атомарной операции. Он начинает с того, что захватывает мьютекс, защищающий счёт Жеки, чтобы изъять деньги, а затем пытается захватить счёт Бяки.
В то же время поток B хочет увести 100$ со счёта Бяки на счёт Жеки. Он получает захват счёта Бяки, чтобы изъять деньги и попытаться захватить счёт Жеки. Оба потока блокируются, уснув в ожидании друг друга.
std::scoped_lock одновременно захватывают (а затем освобождают) все мьютексы, передаваемые в качестве аргумента, по принципу «всё или ничего»: если хотя бы один захват выбрасывает исключение, освобождаются все уже захваченные мьютексы.
Заключение
Если вы вдруг запутались в этом ворохе новой информации:
До встречи в следующей статье, в которой речь пойдёт о condition_variable и вы узнаете, как синхронизировать потоки!
[C++] часть 2: МЬЮТЕКС. Пишем наш первый код для многопоточной среды
Научитесь создавать код для использования в многопоточной среде с помощью реализации потокобезопасной очереди
Nov 30, 2019 · 8 min read
В прошлой статье мы разобрались с тем, что такое конкурентность/параллелизм и зачем нужна синхронизация. Настала пора изучить примитивы синхронизации, которые предлагает нам стандартная библиотека шаблонов C++.
Что такое мьютекс?
Мьютекс (англ. mutex, от mut ual ex clusion — «взаимное исключение») — это базовый механизм синхронизации. Он предназначен для организации взаимоисключающего доступа к общим данным для нескольких потоков с использованием барьеров памяти (для простоты можно считать мьютекс дверью, ведущей к общим данным).
Синтаксис
Как создать потокобезопасную очередь
Разберёмся, как реализовать простейшие потокобезопасные очереди, то есть очереди с безопасным доступом для потоков.
В библиотеке стандартных шаблонов уже есть готовая очередь ( rawQueue ). Наша реализация будет предполагать: а) извлечение и удаление целочисленного значения из начала очереди и б) добавление нового в конец очереди. И всё это при обеспечении потокобезопасности.
Сначала выясним, почему и как эти две операции могут создавать проблемы для многопоточности.
Получается, мы должны быть уверены, что никто не будет трогать очередь, пока мы выполняем наши задачи. Используем мьютекс для защиты этих многоступенчатых операций и сделаем так, чтобы все вместе они смотрелись как одна атомарная операция.
Обратите внимание:
Lock guard и парадигма RAII
У нас две большие проблемы с этим простым мьютексом:
Посмотрим теперь, как можно изменить нашу потокобезопасную очередь threadSafe_queue (на этот раз обращаем внимание на то, где освобождается мьютекс).
Unique lock, дающий свободу
Как только владение мьютексом получено (благодаря std::lock_guard ), он может быть освобождён. std::unique_lock действует в схожей манере плюс делает возможным многократный захват и освобождение (всегда в таком порядке) мьютекса, используя те же преимущества безопасности парадигмы RAII.
Когда использовать?
Общий мьютекс + общий захват дают больше читателей
std::mutex — это мьютекс, которым одномоментно может владеть только один поток. Однако это ограничение не всегда обязательно. Например, потоки могут одновременно и безопасно читать одни и те же общие данные. Просто читать, не производя с ними никаких изменений. Но в случае с доступом к записи только записывающий поток может иметь доступ к данным.
Начиная с C++17, std::shared_mutex формирует доступ двух типов:
Синтаксис
Scoped lock, дающий больше мьютексов (и без клинча)
Краткая история взаимоблокировки:
Поток A хочет увести 200$ с банковского счёта Жеки на счёт Бяки в виде одной атомарной операции. Он начинает с того, что захватывает мьютекс, защищающий счёт Жеки, чтобы изъять деньги, а затем пытается захватить счёт Бяки.
В то же время поток B хочет увести 100$ со счёта Бяки на счёт Жеки. Он получает захват счёта Бяки, чтобы изъять деньги и попытаться захватить счёт Жеки. Оба потока блокируются, уснув в ожидании друг друга.
std::scoped_lock одновременно захватывают (а затем освобождают) все мьютексы, передаваемые в качестве аргумента, по принципу « всё или ничего»: если хотя бы один захват выбрасывает исключение, освобождаются все уже захваченные мьютексы.
Заключение
Если вы вдруг запутались в этом ворохе новой информации:
До встречи в следующей статье, в которой речь пойдёт о condition_variable и вы узнаете, как синхронизировать потоки!
Про многопоточность 1. Thread
Привет. Поймал себя на мысли, что слишком часто приходится обращаться к различным источникам в поисках информации про многопоточность в iOS. Такое себе удовольствие, поэтому собрал все самое полезное и не очень в ряд статей, посвященных многопоточности, ее особенностям и подкапотной жизни. Завариваем чаек и погнали.
Про многопоточность 1. Thread
Про многопоточность 3. NSOperation (coming soon)
Про многопоточность 4. Железяки (coming soon)
Threads
Все операции, выполняемые в мобильном приложении, требуют некоторых ресурсов и имеют время выполнения. Эти операции by default выполняются поочередно в главном потоке.
Главным потоком называется поток, в котором стартует приложение. Обработка событий связанных со взаимодействием с UI происходит на главном потоке (такой операцией может быть обработка тапа по экрану, нажатия на клавишу клавиатуры, движение мыши и тд). Помимо этого, любая операция, написанная нами, будь то выполнение алгоритма, запрос в сеть или обращение к базе данных так же будет выполняться поочередно на главном потоке, что может негативно сказываться на отклике UI. Здесь к нам на помощь приходит многопоточность — возможность выполнять операции параллельно (одновременно) на разных потоках.
Run Loop
И начнем мы, нет, не с потоков, а с части, тесно связанной с их работой, а именно Run Loop. Документация гласит:
A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none
Run Loop — своего рода бесконечный цикл, предназначенный для обработки и координации всех событий, поступающих к нему на вход. В первую очередь стоит отметить, что у каждого потока есть свой ассоциированный Run Loop. Run Loop для главного потока приложение (UIApplication) инициализирует и стартует автоматически, в то время как для созданных потоков запускать и конфигурировать Run Loop необходимо самостоятельно.
Зачем же нужен этот ваш ранлуп? К примеру, Run Loop главного потока отлавливает все системные события и запускает их обработку на главном потоке, будь то нажатия на клавиши клавиатуры, если это macOS, или тап по экрану iOS устройства. Также Run Loop умеет управлять своим потоком: будить для выполнения некоторой работы и переводить в спячку после ее выполнения.
По большому счету, Run Loop — это то, что отличает интерактивное мобильное приложение от обычной программы. Когда Run Loop получает сообщение о событии, он запускает обработчик, ассоциированный с этим событием на своем потоке, а после выполнения усыпляет поток до следующего события, именно таким образом приложение узнает о происходящих интерактивных событиях. Разберемся, какие же события умеет обрабатывать Run Loop:
Существует несколько источников события:
Input sources — различные источники ввода (мышь, клавиатура, тачскрин и тп), кастомные источники (необходимы для передачи сообщений между потоками), а так же вызов performSelector:onThread: (метод, необходимый для вызова события по селектору на определенном потоке)
Timer sources — все таймеры в приложении всегда обрабатываются ранлупом
Как уже говорилось ранее, Run Loop’ом главного потока управляет приложение, в то время, как мы сами управляем Run Loop’ом созданных нами потоков, таким образом мы можем явно указать, какие источники ввода должен обрабатывать Run Loop. Рассмотрим режимы работы Run Loop:
NSDefaultRunLoopMode — режим по умолчанию, который отслеживает все основные события
NSModalPanelRunLoopMode — режим, отслеживающий события в модальных окнах (используется только в macOS)
NSEventTrackingRunLoopMode — режим, который отслеживает системные события связанные с UI (скроллинг, тап по экрану, движение мыши, нажатия на клавиатуре и тп.)
В качестве примера использования режимов Run Loop может быть распространенная проблема, связанная с некорректной работой таймера. Давайте разбираться:
pthread
Рассмотрим аргументы данной функции:
! — указатель на существующий pthread
? — указатель на атрибуты потока, которые позволяют нам настроить поток ( pthread_attr(3) )
_: UnsafeMutableRawPointer? — указатель на аргументы, которые мы хотим передать в функцию
Рассмотрим пример создания pthread :
Поток начнет свое выполнение сразу после создания ( pthread_create ).
Thread
Рассмотрим пример создания потока Thread :
У Thread есть альтернативный способ создания, с использованием Target-Action паттерна:
Quality of service
Существует 5 типов QOS:
Рассмотрим пример использования QOS:
Во фреймворке Foundation так же, как и в pthread API есть возможность приоритезировать задачи с помощью QOS.
Рассмотрим использование QOS в Thread :
Синхронизация
При работе с многопоточностью, часто встает вопрос синхронизации. Существуют ситуации, в которых несколько потоков имеют одновременный доступ к ресурсу, например Thread1 читает ресурс в то время, как Thread2 изменяет его, что приводит к коллизии.
Во избежание таких ситуации существует несколько способов синхронизации, такие как mutex и semaphore. Синхронизация позволяет обеспечить безопасный доступ одного или нескольких потоков к ресурсу.
Mutex
Mutex — примитив синхронизации, позволяющий захватить ресурс. Подразумевается, что как только поток обратиться к ресурсу, захваченному мьютексом, никакой другой поток не сможет с ним взаимодействовать до тех пор, пока текущий поток не освободит этот ресурс
Рассмотрим пример использования pthread mutex:
Стоит отметить, что mutex работает по принципу FIFO, то есть потоки будут захватывать ресурс по освобождению в том порядке, в котором данные потоки обратились к ресурсу.
NSLock
Рассмотрим пример использования NSLock :
Reqursive mutex
Reqursive mutex — разновидность базового mutex, которая позволяет потоку захватывать ресурс множество раз до тех пор, пока он не освободит его. Ядро операционной системы сохраняет след потока, который захватил ресурс и позволяет ему захватывать ресурс повторно. Рекурсивный мьютекс считает количество блокировок и разблокировок, таким образом ресурс будет захвачен до тех пор, пока их количество не станет равно друг другу. Чаще всего используется в рекурсивных функциях.
Рассмотрим пример использования Reqursive mutex:
Если бы в данном примере использовался обычный mutex, поток бы бесконечно ожидал, пока он же сам не освободит ресурс.
NSRecursiveLock
Рассмотрим пример использования NSRecursiveLock :
Condition
Рассмотрим пример использования condition:
NSCondition
Рассмотрим пример использования NSCondition :
Read Write Lock
Read Write Lock — примитив синхронизации, который предоставляет потоку доступ к ресурсу на чтение, в это время закрывая возможность записи в ресурс из других потоков.
Необходимость использовать rwlock появляется тогда, когда много потоков читают данные, и только один поток их пишет (Reader-writers problem). На первый взгляд кажется, что данную проблему можно легко решить простым mutex, однако этот подход будет требовать больше ресурсов, нежели простой rwlock, так как фактически нет необходимости блокировать доступ к ресурсу полностью. rwlock имеет достаточно простое API:
Рассмотри пример практического использования rwlock:
Существует только unix вариация rwlock, во фреймворке Foundation нет альтернативы. В следующей статье мы рассмотрим альтернативы из более высокоуровневых библиотек.
Spin Lock
Spin lock — наиболее быстродействующий, но в то же время энергозатратный и ресурсотребователный mutex. Быстродействие достигается за счет непрерывного опрашивания, освобожден ресурс в данный момент времени или нет.
Рекомендуется использовать spin lock лишь в редких случаях, когда к ресурсу обращается небольшое количество потоков непродолжительное время.
Unfair Lock
Unfair lock (iOS 10+) — примитив многопоточности, позволяющий наиболее эффективно захватывать ресурс. По большому счету, unfair lock является более производительной заменой spin lock. Производительность достигается путем максимального сокращения возможных context switch.
Context switch — процесс переключения между потоками. Для того, чтобы переключаться между потоками, необходимо прекратить работу на текущем потоке, сохранив при этом состояние и всю необходимую информацию, а далее восстановить и загрузить состояние задачи, к выполнению которой переходит процессор. Является энергозатратной и ресурсотребовательной операцией
Вспоминаем, что обычный mutex работает по принципу FIFO, в то время, как unfair lock отдаст предпочтение тому потоку, который чаще обращается к ресурсу, таким образом и достигается сокращение context switch. Имеет достаточно простое API:
Проблемы
Многопоточность предназначена для решения проблем, но как и любые другие технологии может порождать новые. В большинстве случаев проблемы связанны с доступом к ресурсам. Самые распространенные из них:
Deadlock — ситуация, в которой поток бесконечно ожидает доступ к ресурсу, который никогда не будет освобожден
Priority inversion — ситуация, в которой высокоприоритетная задача ожидает выполнения низкоприоритетной задачи.
Race condition — ситуация, в которой ожидаемый порядок выполнения операций становится непредсказуемым, в результате чего страдает закладываемая логика
В данной статье мы рассмотрим только Deadlock, так как остальные проблемы стоит решать используя более высокоуровневые библиотеки.
Deadlock
Deadlock — это ситуация в многозадачной среде, при которой несколько потоков находятся в состоянии ожидания ресурса, занятого друг другом, и ни один из них не может продолжать свое выполнение. Таким образом оба потока бесконечно ожидая друг друга никогда не выполнят задачу, что может привести к неожиданному поведению приложения.
Попробуем воспроизвести самый примитивный кейс бесконечной блокировки ресурса:
Воспроизведем deadlock с использованием вложенных блокировок ресурсов:
Заключение
В заключении хочется сказать, что тема concurrency достаточно большая и сложная, но по мере приближения к высокому уровню, конструкции будут становится проще и понятнее. Скорее всего вам никогда не придется создавать pthread руками, но полезно знать, как работают более высокоуровневые обертки под капотом. В следующих статьях рассмотрим библиотеку GCD, научимся крутить вертеть Operations, прямо как Гудини, а так же копнем еще глубже и изучим все тонкости работы процессора в контексте данной темы. Спасибо за внимание!
Введение в мьютексы
Обзор мьютексов
Р ассмотрим простой пример: несколько потоков обращаются к одной общей переменной. Часть потоков эту переменную увеличивают (plus потоки), а часть уменьшают на единицу (minus потоки). Число plus и minus потоков равно. Таким образом, мы ожидаем, что к концу работы программы значение исходной переменной будет прежним.
Если выполнить код, то он будет возвращать различные значения. Чаще всего они не будут равны нулю. Разберёмся, почему так происходит. Рассмотрим код функций, которые выполняются в отдельных потоках
Во-первых, у нас имеется локальная переменная local. Во вторых, используется тяжёлая и медленная функция printf. В тот момент, когда мы присваиваем локальной переменной значение counter, другой поток может в то же самое время взять это значение и поменять.
Для простоты рассмотрим 4 потока – два plus и два minus
Действие | counter | plus 1 local | plus 2 local | minus 1 local | minus 2 local |
---|---|---|---|---|---|
plus 1 помещает в local значение counter | 0 | 0 | — | — | — |
plus 2 помещает в local значение counter | 0 | 0 | 0 | — | — |
plus 1 инкрементирует local и выводит значение на печать | 0 | 1 | 0 | — | — |
minus 1 помещает в local значение counter | 0 | 1 | 0 | 0 | — |
plus 1 помещает в переменную counter значение local | 1 | 1 | 0 | 0 | — |
minus 2 помещает в local значение counter | 1 | 1 | 0 | 0 | 1 |
plus 2 инкрементирует local и выводит значение на печать | 1 | 1 | 1 | 0 | 1 |
plus 2 помещает в переменную counter значение local | 1 | 1 | 1 | 0 | 1 |
minus 2 декрементирует local и выводит значение на печать | 1 | 1 | 1 | 0 | 0 |
minus 2 помещает в переменную counter значение local | 0 | 1 | 1 | 0 | 0 |
minus 1 декрементирует local и выводит значение на печать | 0 | 1 | 1 | 0 | -1 |
minus 1 помещает в переменную counter значение local | -1 | 1 | 1 | 0 | -1 |
Это один из возможных сценариев развития событий. Очевидно, что могут быть значения от минус 2 до плюс 2. Какой из них будет выполнен, в общем случае не известно.
Проблема заключается в том, что у нас имеется несинхронизированный доступ к общему ресурсу. Мы бы хотели сделать так, чтобы на время работы с ресурсом (всё тело функций minus и plus) к ним имел доступ только один поток, а остальные ждали, пока ресурс освободится. Это так называемое mutual exclusion – взаимное исключение, случай, когда необходимо удостовериться в том, что два (и более…) конкурирующих потока не находятся в критической секции кода одновременно.
В библиотеке pthreads один из методов разрешить эту ситуацию – это мьютексы. Мьютекс – это объект, который может находиться в двух состояниях. Он либо заблокирован (занят, залочен, захвачен) каким-то потоком, либо свободен. Поток, который захватил мьютекс, работает с участком кода. Остальные потоки, когда достигают мьютекса, ждут его разблокировки. Разблокировать мьютекс может только тот поток, который его захватил. Обычно освобождение занятого мьютекса происходит после исполнения критичного к совместному доступу участка кода.
Порядок использования мьютексов
М ьютекс – это экземпляр типа pthread_mutex_t. Перед использованием необходимо инициализировать мьютекс функцией pthread_mutex_init
где первый аргумент – указатель на мьютекс, а второй – аттрибуты мьютекса. Если указан NULL, то используются атрибуты по умолчанию. В случае удачной инициализации мьютекс переходит в состояние «инициализированный и свободный», а функция возвращает 0. Повторная инициализация инициализированного мьютекса приводит к неопределённому поведению.
Если мьютекс создан статически и не имеет дополнительных параметров, то он может быть инициализирован с помощью макроса PTHREAD_MUTEX_INITIALIZER
После использования мьютекса его необходимо уничтожить с помощью функции
В результате функция возвращает 0 в случае успеха или может возвратить код ошибки.
После создания мьютекса он может быть захвачен с помощью функции
После этого участок кода становится недоступным остальным потокам – их выполнение блокируется до тех пор, пока мьютекс не будет освобождён. Освобождение должен провести поток, заблокировавший мьютекс, вызовом
Перейдём к реализации. Для начала сделаем глобальный объект мьютекс, доступный всем потокам.
Заметьте, при использовании мьютекса исполнение защищённого участка кода происходит последовательно всеми потоками, а не параллельно. Порядок доступа отдельных потоков не определён. Напишем теперь реализацию, в которой мьютекс будет передаваться в качестве параметра функции. Для начала, определим новый тип данных
И перепишем фунции
Хочется обратить внимание, что мьютекс один на всех. Если бы у каждого потока был свой собственный мьютекс, то они бы не блокировали работу друг друга. Например, этот код будет работать неправильно
Остановимся подробнее на функциях для работы с мьютексами
Функция может упасть с ошибкой
pthread_mutex_destroy может вернуть следующие ошибки
pthread_nutex_lock возвращает ошибки
pthread_mutex_lock и pthread_mutex_unlock могут вылететь с ошибками