- Критические секции в С++
- Критические секции
- Введение
- Работа с критическими секциями
- Структура RTL_CRITICAL_SECTION
- Классы-обертки для критических секций
- Отладка критических секций
- Ошибки, связанные с реализацией
- Архитектурные ошибки
- Способы обнаружения ошибок
- Сменим тему
- MC++ (управляемый C++)
- Delphi
- Подведем итоги
Критические секции в С++
Теперь я расскажу вам, что такое критическая секция, зачем она нужна и как ее использовать. Конечно же статья будет о многопоточном программировании и увы только под Windows. Я буду использовать Windows7 x64 + VS2010 SP1. Для начала напишем очень простое приложение в несколько потоков.
И так, функция _beginthread как вы наверно уже догадались создает новый поток ее прототип:
Параметр start_address задает функцию потока, параметр stack_size — размер стека ну и arglist — список аргументов передаваемых потоку. Если поток удачно запущен, он вернет его идентификатор(handle).
- Создаем функцию для потока, которая должна возвращать void и принимать в качестве аргументов void* (это для случая с _beginthread);
- В главном потоке(или в любом другом) с помощью _beginthread запускам экземпляры потоков;
Все ну очень просто.
Что же такое критическая секция? Критическая секция — э то объект для синхронизации данных между потоками, то есть она не позволяет выполнять некие действия одновременно. Программист сам решает что «заключить» в критическую секцию. Как с ним работать?
В книге Джеффри Рихтера «Windows для профессионалов», автор приводит такой пример:
Так как я пишу эти строки в самолете, позвольте провести следующую аналогию. Структура CRITICAL_SECTION похожа на туалетную кабинку в самолете, а данные, которые нужно защитить, — на унитаз, Туалетная кабинка (критическая секция) в самолете очень маленькая, и единовременно в ней может находиться только один человек (поток), пользующийся унитазом (защищенным ресурсом).
Если у Вас есть ресурсы, всегда используемые вместе, Вы можете поместить их в одну кабинку — единственная структура CRITICAL_SECTION будет охранять их всех. Но если ресурсы не всегда используются вместе (например, потоки 1 и 2 работают с одним ресурсом, а потоки 1 и 3 — с другим), Вам придется создать им по отдельной кабинке, или структуре CRITICAL_SECTION.
Теперь в каждом участке кода, где Вы обращаетесь к разделяемому ресурсу, вызывайте EnterCriticaSection, передавая ей адрес структуры CRITICAL_SECTION, которая выделена для этого ресурса. Иными словами, поток, желая обратиться к ресурсу, должен сначала убедиться, нет ли на двери кабинки знака «занято». Структура CRITI CAL_SECTION идентифицирует кабинку, в которую хочет войти поток, а функция EnterCriticalSection — тот инструмент, с помощью которого он узнает, свободна или занята кабинка. EnterCriticalSection допустит вызвавший ее поток в кабинку, если определит, что та свободна. В ином случае (кабинка занята) EnterCriticalSection заставит его ждать, пока она не освободится.
Поток, покидая участок кода, где он работал с защищенным ресурсом, должен вызвать функцию LeaveCriticalSection. Тем самым он уведомляет систему о том, что кабинка с данным ресурсом освободилась. Если Вы забудете это сделать, система будет считать, что ресурс все еще занят, и не позволит обратиться к нему другим ждущим потокам, То есть Вы вышли из кабинки и оставили на двери знак «занято».
Давайте создадим простую программу. В ней будет массив, и доступ к нему из разных потоков.
Результат работы программы на изображении справа.
Как видно каждый поток по очереди изменил массив чисел, при этом никаких ошибок по поводу доступа к памяти не возникло и программа отработала корректно!
- Создаем для каждого нужного объекта свою критическую секцию;
- Инициализируем секции перед тем, как работать с ними;
- Для того, что бы начать работу с объектом вызываем функцию EnterCriticalSection и передаем ей связанную(абстрактно) с объектом критическую секцию;
- После окончания работы с объектом, выходим из необходимой критической секции с помощью функции LeaveCriticalSection;
- Когда уже точно известно, что потоки больше не будут использовать критическую секцию, можем ее удалить с помощью функции DeleteCriticalSection.
- ПРОФИТ!
Источник
Критические секции
Автор: Павел Блудов
The RSDN Group
Источник: RSDN Magazine #6-2004
Опубликовано: 14.03.2005
Исправлено: 10.12.2016
Версия текста: 1.2
Введение
Критические секции — это объекты, используемые для блокировки доступа всех нитей (threads) приложения, кроме одной, к некоторым важным данным в один момент времени. Например, имеется переменная m_pObject и несколько нитей, вызывающих методы объекта, на который ссылается m_pObject, причем эта переменная может изменять свое значение время от времени. Иногда там даже оказывается нуль. Предположим, имеется вот такой код:
Тут мы имеем потенциальную опасность вызова m_pObject->SomeMethod() после того, как объект был уничтожен при помощи delete m_pObject . Дело в том, что в системах с вытесняющей многозадачностью выполнение любой нити процесса может прерваться в самый неподходящий для нее момент времени, и начнет выполняться совершенно другая нить. В данном примере неподходящим моментом будет тот, в котором нить №1 уже проверила m_pObject , но еще не успела вызвать SomeMethod() . Выполнение нити №1 прервалось, и начала исполняться нить №2. Причем нить №2 успела вызвать деструктор объекта. Что же произойдет, когда нить №1 получит немного процессорного времени и вызовет-таки SomeMethod() у уже несуществующего объекта? Наверняка что-то ужасное.
Именно тут приходят на помощь критические секции. Перепишем наш пример.
Код, помещенный между ::EnterCriticalSection() и ::LeaveCriticalSection() с одной и той же критической секцией в качестве параметра, никогда не будет выполняться параллельно. Это означает, что если нить №1 успела «захватить» критическую секцию m_lockObject, то при попытке нити №2 заполучить эту же критическую секцию в свое единоличное пользование, ее выполнение будет приостановлено до тех пор, пока нить №1 не «отпустит» m_lockObject при помощи вызова ::LeaveCriticalSection(). И наоборот, если нить №2 успела раньше нити №1, то та «подождет», прежде чем начнет работу с m_pObject .
Работа с критическими секциями
Что же происходит внутри критических секций и как они устроены? Прежде всего, следует отметить, что критические секции – это не объекты ядра операционной системы. Практически вся работа с критическими секциями происходит в создавшем их процессе. Из этого следует, что критические секции могут быть использованы только для синхронизации в пределах одного процесса. Теперь рассмотрим критические секции поближе.
Структура RTL_CRITICAL_SECTION
Поле LockCount увеличивается на единицу при каждом вызове ::EnterCriticalSection() и уменьшается при каждом вызове ::LeaveCriticalSection(). Это первая (а часто и единственная проверка) на пути к «захвату» критической секции. Если после увеличения в этом поле находится ноль, это означает, что до этого момента непарных вызовов ::EnterCriticalSection() из других ниток не было. В этом случае можно забрать данные, охраняемые этой критической секцией в монопольное пользование. Таким образом, если критическая секция интенсивно используется не более чем одной нитью, ::EnterCriticalSection() практически вырождается в ++LockCount , а ::LeaveCriticalSection() в —LockCount . Это очень важно. Это означает, что использование многих тысяч критических секций в одном процессе не повлечет значительного расхода ни системных ресурсов, ни процессорного времени.
СОВЕТ Не стоит экономить на критических секциях. Много 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. И, наконец, поле SpinCount . Это поле используется только многопроцессорными системами. В однопроцессорных системах, если критическая секция занята другой нитью, можно только переключить управление на нее и подождать наступления события. В многопроцессорных системах есть альтернатива: прогнать некоторое количество раз холостой цикл, проверяя каждый раз, не освободилась ли наша критическая секция. Если за SpinCount раз это не получилось, переходим к ожиданию. Это гораздо эффективнее, чем переключение на планировщик ядра и обратно. Кроме того, в WindowsNT/2k старший бит этого поля служит для индикации того, что объект ядра, хендл которого находится в поле LockSemaphore, должен быть создан заранее. Если системных ресурсов для этого недостаточно, система сгенерирует исключение, и программа может «урезать» свою функциональность. Или совсем завершить работу.
|