Что такое критическая секция

FreeRTOS: мьютексы и критические секции

Мьютексы.

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

Рисунок 4. После окончания работы таск В возвращает жетон и в результате другие таски могут получить доступ к ресурсу.
Для создания мьютекса используется специальная API функция:

Которая возвращает созданный мьютекс, или NULL, если недостаточно памяти.
Следующий код показывает механизм работы с мьютексом:

Критические секции.

Что если по ходу работы логики Вашего кода, необходим короткий участок кода, выполнение которого не должно быть прервано переключением на другой таск (примером такого участка может послужить запись какого-либо значения в порт). Для этого в FreeRTOS используются специальные макросы:

Во время выполнения этого участка кода не может быть произведено переключение контекста (т.е. переключение на другую задачу), но могут приходить прерывания на версиях FreeRTOS с поддержкой вложенных прерываний, но не все, а только те у которых приоритет выше константы configMAX_SYSCALL_INTERRUPT_PRIORITY.

Другой подход построения критических секций — приостановка работы планировщика. Разница лишь в том, что критическая секция, построенная на макросах, защищает выполняемый участок кода от совместного доступа других тасков, и прерываний, а критическая секция, построенная на приостановке планировщика — только от других тасков.
Для этого используют 2 API функции:

При этом значение, которое возвращает функция xTaskResumeAll(); указывает на необходимость выполнения принудительного переключения контекста с помощью taskYIELD();

Инверсия приоритетов и тупики.

В прошлой статье я вскользь упомянул о gatekeeper task, как о решении проблемы инверсии приоритетов и попадания тасков в тупик. Здесь я бы более подробно хотел описать, что это такое.

Инверсия приоритетов — это ситуация при которой, Task A имеющий более высокий приоритет, чем Task B ожидает завершения его работы т.к. он первым захватил „жетон» (мьютекс).

Пожалуй, на этом все, интересно было бы услышать поправки по всему циклу, дабы скорректировать статьи, и в дальнейшем ссылаться на них.
Для дальнейшего изучения я бы, прежде всего, посоветовал прочитать оригинальное руководство «Using the FreeRTOS real time kernel», информация из которого использовалась при написании статей, а также документацию на официальном сайте.

Источник

Алгоритмы синхронизации

Критическая секция

Здесь критический участок для каждого процесса – от операции «Обнаруживает, что хлеба нет» до операции «Возвращается в комнату» включительно. В результате отсутствия взаимоисключения мы из ситуации «Нет хлеба» попадаем в ситуацию «Слишком много хлеба». Если бы этот критический участок выполнялся как атомарная операция – «Достает два батона хлеба», то проблема образования излишков была бы снята.

Таблица 5.1.

ВремяСтудент 1Студент 2Студент 3
17-05Приходит в комнату
17-07Обнаруживает,что хлеба нет
17-09Уходит в магазин
17-11Приходит в комнату
17-13Обнаруживает, что хлеба нет
17-15Уходит в магазин
17-17Приходит в комнату
17-19Обнаруживает,что хлеба нет
17-21Уходит в магазин
17-23Приходит в магазин
17-25Покупает 2 батона на всех
17-27Уходит из магазина
17-29Приходит в магазин
17-31Покупает 2 батона на всех
17-33Уходит из магазина
17-35Приходит в магазин
17-37Покупает 2 батона на всех
17-39Уходит из магазина
17-41Возвращается в комнату
17-43
17-45
17-47Возвращается в комнату
17-49
17-51
17-53Возвращается в комнату

Сделать процесс добывания хлеба атомарной операцией можно было бы следующим образом: перед началом этого процесса закрыть дверь изнутри на засов и уходить добывать хлеб через окно, а по окончании процесса вернуться в комнату через окно и отодвинуть засов. Тогда пока один студент добывает хлеб, все остальные находятся в состоянии ожидания под дверью ( таблица 5.2).

Таблица 5.2.

ВремяСтудент 1Студент 2Студент 3
17-05Приходит в комнату
17-07Достает два батона хлеба
17-43Приходит в комнату
17-47Приходит в комнату

В общем случае структура процесса, участвующего во взаимодействии, может быть представлена следующим образом:

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

Источник

Национальная библиотека им. Н. Э. Баумана
Bauman National Library

Персональные инструменты

Критическая секция

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

Содержание

Критическая секция

Необходимость Критической секции

Различные коды или процессы могут иметь одну и ту же переменную или другие ресурсы, которые необходимо прочитать или записать, но результаты которых зависят от порядка, в котором происходят действия. Например, если переменная «x» должна быть прочитана процессом A, и процесс B должен записывать ту же переменную «x» в то же время, процесс A может получить либо старое, либо новое значение «x».

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

В таких случаях важна критическая секция. В приведенном выше случае, если A необходимо прочитать обновленное значение «x», выполнение процесса A и процесса B одновременно может не дать требуемых результатов. Чтобы предотвратить это, переменная «х» защищена критической секцией. Сначала, B получает доступ к секции. Когда B заканчивает запись значения, A получает доступ к критической секции, и переменная «x» может быть прочитана.

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

Реализация критической секции

Что такое критическая секция. Смотреть фото Что такое критическая секция. Смотреть картинку Что такое критическая секция. Картинка про Что такое критическая секция. Фото Что такое критическая секция

Чтобы избежать любого изменения управлением процессора внутри критической секции в унифицированных процессорных системах можно просто, отключить прерывания при входе в критический раздел, избегая системных вызовов, которые могут вызвать переключение контекста внутри раздела и восстановления прерываний с возвратом в первоначальное состояние на выходе. Любой выполняющийся поток, войдя в любую критическую секцию в любой точке системы, с данной реализацией предотвратит предоставление времени но обработку процессором(CPU) любому другому потоку, и, следовательно, от входа в любую другую критическую секцию или, любой другой код до тех пор, пока исходный процесс не покинет свою критическую секцию.

Этот грубый подход можно улучшить с помощью семафоров. Чтобы войти в критическую секцию, поток должен попасть в семафор, который он освобождает при выходе из секции. Другие потоки не могут войти в критическую секцию одновременно с исходным потоком, но могут свободно управлять процессором и выполнять другой код, включая другие критические секции, которые защищены различными семафорами. Блокировка семафора также имеет ограничение по времени, чтобы предотвратить условие взаимоблокировки, при котором шлюз занимается одним процессом в течение бесконечного времени, останавливая другие процессы, которые должны использовать общий ресурс, защищенный критической секцией.

Критические секции уровня ядра

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

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

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

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

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

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

Критические секции уровня ядра являются основой проблемой локаута программного обеспечения.

Критические секции в разных языках

Тут имеется схожий с C++ API, но под другими именами.

Критические секции представлены классом System.Threading.Monitor, вместо ::EnterCriticalSection() есть Monitor.Enter(object), а вместо ::LeaveCriticalSection() Monitor.Exit(object), где object – это любой объект C#. Т.е. каждый объект где-то в потрохах CLR (Common Language Runtime) имеет свою собственную критическую секцию либо заводит ее по необходимости. Типичное использование этой секции выглядит так:

Примечание: CLR – это не только C#, все это применимо и к другим языкам, использующим CLR.

MC++ (управляемый C++)

Delphi

Практически все, что верно для C++, верно и для Delphi. Критические секции представлены объектом TCriticalSection. Собственно, это такая же обертка.

Кроме того, в Delphi присутствует специальный объект TMultiReadExclusiveWriteSynchronizer с названием, говорящим само за себя.

Источник

Критические секции


Автор: Павел Блудов
The RSDN Group
Источник: RSDN Magazine #6-2004

Опубликовано: 14.03.2005
Исправлено: 10.12.2016
Версия текста: 1.2

Введение

Именно тут приходят на помощь критические секции. Перепишем наш пример.

Работа с критическими секциями

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

Структура RTL_CRITICAL_SECTION

СОВЕТ

Не стоит экономить на критических секциях. Много cэкономить все равно не получится.

В поле RecursionCount хранится количество повторных вызовов ::EnterCriticalSection() из одной и той же нити. Действительно, если вызвать ::EnterCriticalSection() из одной и той же нити несколько раз, все вызовы будут успешны. Т.е. вот такой код не остановится навечно во втором вызове ::EnterCriticalSection(), а отработает до конца.

Действительно, критические секции предназначены для защиты данных от доступа из нескольких ниток. Многократное использование одной и той же критической секции из одной нити не приведет к ошибке. Это вполне нормальное явление. Следите, чтобы количество вызовов ::EnterCriticalSection() и ::LeaveCriticalSection() совпадало, и все будет хорошо.

Поле OwningThread содержит 0 для никем не занятых критических секций или уникальный идентификатор нити-владельца. Это поле проверяется, если при вызове ::EnterCriticalSection() поле LockCount после увеличения на единицу оказалось больше нуля. Если OwningThread совпадает с уникальным идентификатором текущей нити, то RecursionCount просто увеличивается на единицу и ::EnterCriticalSection() возвращается немедленно. Иначе ::EnterCriticalSection() будет дожидаться, пока нить, владеющая критической секцией, не вызовет ::LeaveCriticalSection() необходимое количество раз.

Поле LockSemaphore используется, если нужно подождать, пока критическая секция освободится. Если LockCount больше нуля, и OwningThread не совпадает с уникальным идентификатором текущей нити, то ждущая нить создает объект ядра (событие) и вызывает ::WaitForSingleObject( LockSemaphore ). Нить-владелец, после уменьшения RecursionCount, проверяет его, и если значение этого поля равно нулю, а LockCount больше нуля, то это значит, что есть как минимум одна нить, ожидающая, пока LockSemaphore не окажется в состоянии «случилось!». Для этого нить-владелец вызывает ::SetEvent(), и какая-то одна ( только одна ) из ожидающих ниток пробуждается и получает доступ к критическим данным.

WindowsNT/2k генерирует исключение, если попытка создать событие не увенчалась успехом. Это верно как для функций ::Enter/LeaveCriticalSection(), так и для ::InitializeCriticalSectionAndSpinCount() с установленным старшим битом параметра SpinCount. Но только не в WindowsXP. Разработчики ядра этой операционной системы поступили по-другому. Вместо генерации исключения, функции ::Enter/LeaveCriticalSection(), если не могут создать собственное событие, начинают использовать заранее созданный глобальный объект. Один на всех. Таким образом, в случае катастрофической нехватки системных ресурсов, программа под управлением WindowsXP ковыляет какое-то время дальше. Действительно, писать программы, способные продолжать работать после того, как ::EnterCriticalSection() сгенерировала исключение, чрезвычайно сложно. Как правило, если программистом и предусмотрен такой поворот событий, то дальше вывода сообщения об ошибке и аварийного завершения программы дело не идет. Как следствие, WindowsXP игнорирует старший бит поля LockCount.

ПРИМЕЧАНИЕ

Все это верно для Windows NT/2k/XP. В Windows 9x/Me используется только поле LockCount. Там находится указатель на объект ядра, возможно, просто взаимоисключение (mutex). Все остальные поля равны нулю.

API для работы с критическими секциями

BOOL InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

Заполняют поля структуры, адресуемой lpCriticalSection. После вызова любой из этих функций критическая секция готова к работе.

Устанавливает значение поля SpinCount и возвращает его предыдущее значение. Напоминаю, что старший бит отвечает за «привязку» события, используемого для ожидания доступа к данной критической секции.

VOID DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

Освобождает ресурсы, занимаемые критической секцией.

VOID EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

BOOL TryEnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

Осуществляют «захват» критической секции. Если критическая секция занята другой нитью, то ::EnterCriticalSection() будет ждать, пока та освободится, а ::TryEnterCriticalSection() вернет FALSE. Отсутствует в Windows 9x/ME.

VOID LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

Освобождает критическую секцию,

Классы-обертки для критических секций

Классы CLock и CAutoLock удобно использовать для синхронизации доступа к переменным класса, а CScopeLock предназначен, в основном, для использования в процедурах. Удобно, что компилятор сам позаботится о вызове ::LeaveCriticalSection() через деструктор.

Отладка критических секций

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

Ошибки, связанные с реализацией

Это довольно легко обнаруживаемые ошибки, как правило, связанные с непарностью вызовов ::EnterCriticalSection() и ::LeaveCriticalSection().

::LeaveCriticalSection() без ::EnterCriticalSection() приведет к тому, что первый же вызов ::EnterCriticalSection() остановит выполнение нити навсегда.

В этом примере, конечно, имеет смысл воспользоваться классом типа CScopeLock.

Кроме того, случается, что ::EnterCriticalSection() вызывается без инициализации критической секции с помощью ::InitializeCriticalSection(). Особенно часто такое случается с проектами, написанными с помощью ATL. Причем в debug-версии все работает замечательно, а release-версия рушится. Это происходит из-за так называемой «минимальной» CRT (_ATL_MIN_CRT), которая не вызывает конструкторы статических объектов (Q166480, Q165076). В ATL версии 7.0 эту проблему решили.

Еще я встречал такую ошибку: программист пользовался классом типа CScopeLock, но для экономии места называл эту переменную одной буквой:

и как-то раз просто пропустил имя у переменной. Получилось

Что это означает? Компилятор честно сделал вызов конструктора CScopeLock и тут же уничтожил этот безымянный объект, как и положено по стандарту. Т.е. сразу же после вызова метода Lock() последовал вызов Unlock(), и синхронизация перестала иметь место. Вообще, давать переменным, даже локальным, имена из одной буквы – путь быстрого наступления на всяческие грабли.

СОВЕТ

Если у вас в процедуре больше одного цикла, то вместо int i,j,k стоит все-таки использовать что-то вроде int nObject, nSection, nRow.

Архитектурные ошибки

Самая известная из них – это взаимоблокировка (deadlock), когда две нити пытаются захватить две или более критических секций, причем делают это в разном порядке.

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

Из такого присвоения трудно извлечь какую-либо пользу. А вот такой код иногда пишут:

и все бы хорошо, если бы у структуры SData был конструктор копирования, например такой:

Но нет, программист посчитал, что хватит за глаза простого копирования полей, и, в результате, переменная m_lock была просто скопирована, хотя именно в этот момент из другой нити она была «захвачена», и значение поля LockCount у нее в этот момент больше либо равно нулю. После вызова ::LeaveCriticalSection() в той нити, у исходной переменной m_lock значение поля LockCount уменьшилось на единицу. А у скопированной переменной – осталось прежним. И любой вызов ::EnterCriticalSection() в этой нити никогда не вернется. Он будет вечно ждать неизвестно чего.

Это только цветочки. С ягодками вы очень быстро столкнетесь, если попытаетесь написать что-нибудь действительно сложное. Например, ActiveX-объект в многопоточном подразделении (MTA), создаваемый из скрипта, запущенного из-под контейнера, размещенного в однопоточном подразделении (STA). Ни слова не понятно? Не беда. Сейчас я попытаюсь выразить проблему более понятным языком. Итак. Имеется объект, вызывающий методы другого объекта, причем живут они в разных нитях. Вызовы производятся синхронно. Т.е. объект №1 переключает выполнение на нить объекта №2, вызывает метод и переключается обратно на свою нить. При этом выполнение нити №1 приостановлено до тех пор, пока не отработает нить объекта №2. Теперь, положим, объект №2 вызывает метод объекта №1 из своей нити. Получается, что управление вернулось в объект №1, но из нити объекта №2. Если объект №1 вызывал метод объекта №2, захватив какую-либо критическую секцию, то при вызове метода объекта №1 тот заблокирует сам себя при повторном входе в ту же критическую секцию.

Если бы в примере не было переключения нитей, все вызовы произошли бы в нити объекта №1, и никаких проблем не возникло. Сильно надуманный пример? Ничуть. Именно переключение ниток лежит в основе подразделений (apartments) COM. А из этого следует одно очень, очень неприятное правило.

СОВЕТ

Избегайте вызовов каких бы то ни было объектов при захваченных критических секциях.

Помните пример из начала статьи? Так вот, он абсолютно неприемлем в подобных случаях. Его придется переделать на что-то вроде примера, приведенного в листинге 12.

Доступ к объекту по-прежнему синхронизован, но вызов SomeMethod(); происходит вне критической секции. Победа? Почти. Осталась одна маленькая деталь. Давайте посмотрим, что происходит в Proc2():

Очевидно, что вызовы m_pObject.p->AddRef(); и m_pObject.p->Release(); происходят внутри критической секции. И если вызов метода AddRef(), как правило, безвреден, то вызов метода Release() может оказаться последним вызовом Release(), и объект самоуничтожится. В методе FinalRelease() объекта №2 может быть все что угодно, например, освобождение объектов, живущих в других подразделениях. А это опять приведет к переключению ниток и может вызвать самоблокировку объекта №1 по уже известному сценарию. Придется воспользоваться той же техникой, что и в методе Proc1():

Теперь потенциально последний вызов IObject2::Release() будет осуществлен после выхода из критической секции. А присвоение нового значения по-прежнему синхронизовано с вызовом IObject2::SomeMethod() из нити №1.

Способы обнаружения ошибок

Ну, а какие у нас альтернативы? Да, пожалуй, только одна. Не использовать API для работы с критическими секциями. Вместо них написать свои собственные. Пусть даже не такие обточенные напильником, как в Windows NT. Не страшно. Нам это понадобится только в debug-конфигурациях. В release’ах мы будем продолжать использовать оригинальный API от Майкрософт. Для этого напишем несколько функций, полностью совместимых по типам и количеству аргументов с «настоящим» API, и добавим #define, как у MFC, для переопределения оператора new в debug-конфигурациях.

Ну и заодно добавим еще один метод в наш класс Clock (листинг 15).

Использовать метод Check() в release-конфигурациях не стоит, возможно, что в будущем, в какой-нибудь Windows64, структура RTL_CRITICAL_SECTION изменится, и результат такой проверки будет не определен. Так что ему самое место «жить» внутри всяческих ASSERT’ов.

Итак, что мы имеем? Мы имеем проверку на лишний вызов ::LeaveCriticalSection() и ту же трассировку для блокировок. Не так уж много. Особенно если трассировка о блокировке имеет место, а вот нить, забывшая освободить критическую секцию, давно завершилась. Как быть? Вернее, что бы еще придумать, чтобы ошибку проще было выявить? Как минимум, прикрутить сюда __LINE__ и __FILE__, константы, соответствующие текущей строке и имени файла на момент компиляции этого метода.

Компилируем, запускаем. Результат удивительный. Хотя правильный. Компилятор честно подставил номер строки и имя файла, соответствующие началу нашей EnterCriticalSectionDbg(). Так что придется попотеть немного больше. __LINE__ и __FILE__ нужно вставить в #define’ы, тогда мы получим действительные номер строки и имя исходного файла. Теперь вопрос, куда же сохранить эти параметры для дальнейшего использования? Причем хочется оставить за собой возможность вызова стандартных функций API наряду с нашими собственными? На помощь приходит C++: просто создадим свою структуру, унаследовав ее от RTL_CRITICAL_SECTION (листинг 16).

Приводим наши классы в соответствие (листинг 17).

Сменим тему

А что это мы все про Win32 API да про C++? Давайте посмотрим, как обстоят дела с критическими секциями в более современных языках программирования.

Тут стараниями Майкрософт имеется полный набор старого доброго API под новыми именами.

А вот Monitor.TryEnter() в C# (о, чудо!) принимает в качестве параметра максимальный период ожидания.

Замечу, что CLR – это не только C#, все это применимо и к другим языкам, использующим CLR.

MC++ (управляемый C++)

Тут тоже появился атрибут [synchronized] ведущий себя точно так же, как и одноименное ключевое слово из Java. Странно, что архитекторы из Майкрософт решили позаимствовать синтаксис из продукта от Sun Microsystems вместо своего собственного.

Delphi

Кроме того, в Delphi присутствует специальный объект TMultiReadExclusiveWriteSynchronizer с названием, говорящим само за себя.

Подведем итоги

Итак, что нужно знать о критических секциях:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *