- CRITICAL_SECTION и избегать включения windows.h?
- Решение
- Другие решения
- Critical Section Objects
- Критические секции
- Введение
- Работа с критическими секциями
- Структура RTL_CRITICAL_SECTION
- Классы-обертки для критических секций
- Отладка критических секций
- Ошибки, связанные с реализацией
- Архитектурные ошибки
- Способы обнаружения ошибок
- Сменим тему
- MC++ (управляемый C++)
- Delphi
- Подведем итоги
CRITICAL_SECTION и избегать включения windows.h?
У меня есть интерфейс, объявленный так:
Проблема в том, что мне нужно включить windows.h для определения CRITICAL_SECTION;
но разве это не может привести к тому, что заголовок может вызвать проблемы с другими, использующими интерфейс?
Как я могу объявить свой typedef без необходимости включать весь windows.h?
Решение
Типичный способ предотвратить утечку деталей реализации в другой код — это использовать Pimpl Idiom. Идея состоит в том, чтобы ваш класс просто содержал указатель на реальную реализацию. Поскольку реальная реализация находится в файле .cpp, она может включать в себя все, что ей нужно, без загрязнения пространства имен пользователей класса.
В заголовочном файле:
Затем в классе реализации (.cpp):
Вы можете поместить всю реализацию в блоки #ifdef или в отдельные файлы .cpp (например, mutex_win.cpp, mutex_posix.cpp и т. Д.) И просто использовать правильную для вашего типа сборки.
В целом, Pimpl Idiom требует дополнительной разыменования указателя, как и ваше решение для виртуальных методов.
Другие решения
Я бы предпочел немного более грубое разделение путей кода, может быть так:
Таким образом, каждый отдельный класс достаточно прост для проверки и рефакторинга, и вы по-прежнему получаете зависящие от платформы реализации. Или, если вы найдете реализацию pthreads для Windows, вы можете даже использовать оба одновременно.
Я думаю, что основная проблема заключается в том, что вы подвергаете тип, специфичный для операционной системы, тому, что может быть не специфичным для операционной системы кодом.
Одним из решений, в зависимости от ваших потребностей, может быть исключение GetMutexHandle функция от IMutex интерфейс, но включить его в два подинтерфейса, IMutexWin32 а также IMutexPosix сказать. Вы могли бы объявить IMutexWin32 интерфейс только для кода, который специально запросил его, и требует, чтобы такой код включал Windows.h , Любой код, который будет фактически использовать GetMutexHandle на винде понадобится Windows.h тем не мение.
В качестве альтернативы, вы можете сделать GetMutexHandle вернуть отдельный интерфейсный класс с специфичными для ОС подынтерфейсами.
Незначительный вариант той же темы будет определять, был ли включен Windows.h ( #ifdef _WINDOWS_ ) или нет и используйте это, чтобы решить, следует ли объявлять специфичный для Windows подинтерфейс.
Самое простое (но самое уродливое) решение из всех было бы сделать GetMutexHandle вернуть указатель на void и доверить вызывающей стороне правильное приведение.
Critical Section Objects
A critical section object provides synchronization similar to that provided by a mutex object, except that a critical section can be used only by the threads of a single process. Critical section objects cannot be shared across processes.
Event, mutex, and semaphore objects can also be used in a single-process application, but critical section objects provide a slightly faster, more efficient mechanism for mutual-exclusion synchronization (a processor-specific test and set instruction). Like a mutex object, a critical section object can be owned by only one thread at a time, which makes it useful for protecting a shared resource from simultaneous access. Unlike a mutex object, there is no way to tell whether a critical section has been abandoned.
Starting with Windows ServerВ 2003 with Service PackВ 1 (SP1), threads waiting on a critical section do not acquire the critical section on a first-come, first-serve basis. This change increases performance significantly for most code. However, some applications depend on first-in, first-out (FIFO) ordering and may perform poorly or not at all on current versions of Windows (for example, applications that have been using critical sections as a rate-limiter). To ensure that your code continues to work correctly, you may need to add an additional level of synchronization. For example, suppose you have a producer thread and a consumer thread that are using a critical section object to synchronize their work. Create two event objects, one for each thread to use to signal that it is ready for the other thread to proceed. The consumer thread will wait for the producer to signal its event before entering the critical section, and the producer thread will wait for the consumer thread to signal its event before entering the critical section. After each thread leaves the critical section, it signals its event to release the other thread.
Windows ServerВ 2003 and WindowsВ XP: Threads that are waiting on a critical section are added to a wait queue; they are woken and generally acquire the critical section in the order in which they were added to the queue. However, if threads are added to this queue at a fast enough rate, performance can be degraded because of the time it takes to awaken each waiting thread.
The process is responsible for allocating the memory used by a critical section. Typically, this is done by simply declaring a variable of type CRITICAL_SECTION. Before the threads of the process can use it, initialize the critical section by using the InitializeCriticalSection or InitializeCriticalSectionAndSpinCount function.
A thread uses the EnterCriticalSection or TryEnterCriticalSection function to request ownership of a critical section. It uses the LeaveCriticalSection function to release ownership of a critical section. If the critical section object is currently owned by another thread, EnterCriticalSection waits indefinitely for ownership. In contrast, when a mutex object is used for mutual exclusion, the wait functions accept a specified time-out interval. The TryEnterCriticalSection function attempts to enter a critical section without blocking the calling thread.
When a thread owns a critical section, it can make additional calls to EnterCriticalSection or TryEnterCriticalSection without blocking its execution. This prevents a thread from deadlocking itself while waiting for a critical section that it already owns. To release its ownership, the thread must call LeaveCriticalSection one time for each time that it entered the critical section. There is no guarantee about the order in which waiting threads will acquire ownership of the critical section.
A thread uses the InitializeCriticalSectionAndSpinCount or SetCriticalSectionSpinCount function to specify a spin count for the critical section object. Spinning means that when a thread tries to acquire a critical section that is locked, the thread enters a loop, checks to see if the lock is released, and if the lock is not released, the thread goes to sleep. On single-processor systems, the spin count is ignored and the critical section spin count is set to 0 (zero). On multiprocessor systems, if the critical section is unavailable, the calling thread spins dwSpinCount times before performing a wait operation on a semaphore that is associated with the critical section. If the critical section becomes free during the spin operation, the calling thread avoids the wait operation.
Any thread of the process can use the DeleteCriticalSection function to release the system resources that are allocated when the critical section object is initialized. After this function is called, the critical section object cannot be used for synchronization.
When a critical section object is owned, the only other threads affected are the threads that are waiting for ownership in a call to EnterCriticalSection. Threads that are not waiting are free to continue running.
Критические секции
Автор: Павел Блудов
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, должен быть создан заранее. Если системных ресурсов для этого недостаточно, система сгенерирует исключение, и программа может «урезать» свою функциональность. Или совсем завершить работу.
|