Критические секции
Критические секции
Павел Блудов
Введение
Критические секции -- это объекты, используемые для
блокировки доступа всех нитей (threads) приложения, кроме одной, к некоторым
важным данным в один момент времени. Например, имеется переменная m_pObject и
несколько нитей, вызывающих методы объекта, на который ссылается m_pObject,
причем эта переменная может изменять свое значение время от времени. Иногда там
даже оказывается нуль. Предположим, имеется вот такой код:
// Нить
№1
void Proc1()
{
if
(m_pObject)
m_pObject->SomeMethod();
}
// Нить
№2
void
Proc2(IObject *pNewObject)
{
if
(m_pObject)
delete
m_pObject;
m_pObject = pNewobject;
}
|
Тут мы имеем потенциальную опасность вызова
m_pObject->SomeMethod() после того, как объект был уничтожен при помощи
delete m_pObject. Дело в том, что в системах с вытесняющей многозадачностью
выполнение любой нити процесса может прерваться в самый неподходящий для нее
момент времени, и начнет выполняться совершенно другая нить. В данном примере
неподходящим моментом будет тот, в котором нить №1 уже проверила m_pObject, но
еще не успела вызвать SomeMethod(). Выполнение нити №1 прервалось, и начала
исполняться нить №2. Причем нить №2 успела вызвать деструктор объекта. Что же
произойдет, когда нить №1 получит немного процессорного времени и вызовет-таки
SomeMethod() у уже несуществующего объекта? Наверняка что-то ужасное.
Именно тут приходят на помощь критические секции.
Перепишем наш пример.
// Нить
№1
void Proc1()
{
::EnterCriticalSection(&m_lockObject);
if
(m_pObject)
m_pObject->SomeMethod();
::LeaveCriticalSection(&m_lockObject);
}
// Нить
№2
void
Proc2(IObject *pNewObject)
{
::EnterCriticalSection(&m_lockObject);
if (m_pObject)
delete
m_pObject;
m_pObject =
pNewobject;
::LeaveCriticalSection(&m_lockObject);
}
|
Код, помещенный между ::EnterCriticalSection() и
::LeaveCriticalSection() с одной и той же критической секцией в качестве
параметра, никогда не будет выполняться параллельно. Это означает, что если
нить №1 успела "захватить" критическую секцию m_lockObject, то при
попытке нити №2 заполучить эту же критическую секцию в свое единоличное
пользование, ее выполнение будет приостановлено до тех пор, пока нить №1 не
"отпустит" m_lockObject при помощи вызова ::LeaveCriticalSection(). И
наоборот, если нить №2 успела раньше нити №1, то та "подождет",
прежде чем начнет работу с m_pObject.
Работа с критическими секциями
Что же происходит внутри критических секций и как они
устроены? Прежде всего, следует отметить, что критические секции – это не
объекты ядра операционной системы. Практически вся работа с критическими
секциями происходит в создавшем их процессе. Из этого следует, что критические
секции могут быть использованы только для синхронизации в пределах одного
процесса. Теперь рассмотрим критические секции поближе.
Структура RTL_CRITICAL_SECTION
typedef struct
_RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // Используется операционной системой
LONG LockCount; // Счетчик использования этой
критической секции
LONG RecursionCount;
// Счетчик повторного захвата из нити-владельца
HANDLE OwningThread;
// Уникальный ID нити-владельца
HANDLE
LockSemaphore; // Объект ядра используемый для ожидания
ULONG_PTR SpinCount;
// Количество холостых циклов перед вызовом ядра
}
RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
|
Поле LockCount увеличивается на единицу при каждом
вызове ::EnterCriticalSection() и уменьшается при каждом вызове
::LeaveCriticalSection(). Это первая (а часто и единственная проверка) на пути
к "захвату" критической секции. Если после увеличения в этом поле
находится ноль, это означает, что до этого момента непарных вызовов ::EnterCriticalSection()
из других ниток не было. В этом случае можно забрать данные, охраняемые этой
критической секцией в монопольное пользование. Таким образом, если критическая
секция интенсивно используется не более чем одной нитью,
::EnterCriticalSection() практически вырождается в ++LockCount, а
::LeaveCriticalSection() в --LockCount. Это очень важно. Это означает, что
использование многих тысяч критических секций в одном процессе не повлечет
значительного расхода ни системных ресурсов, ни процессорного времени.
СОВЕТ
Не стоит экономить на критических
секциях. Много cэкономить все равно не получится.
|
В поле RecursionCount хранится количество повторных
вызовов ::EnterCriticalSection() из одной и той же нити. Действительно, если
вызвать ::EnterCriticalSection() из одной и той же нити несколько раз, все
вызовы будут успешны. Т.е. вот такой код не остановится навечно во втором
вызове ::EnterCriticalSection(), а отработает до конца.
// Нить
№1
void Proc1()
{
::EnterCriticalSection(&m_lock);
//. ..
Proc2()
//. ..
::LeaveCriticalSection(&m_lock);
}
// Все еще нить
№1
void Proc2()
{
::EnterCriticalSection(&m_lock);
//. ..
::LeaveCriticalSection(&m_lock);
}
|
Действительно, критические секции предназначены для
защиты данных от доступа из нескольких ниток. Многократное использование одной
и той же критической секции из одной нити не приведет к ошибке. Это вполне
нормальное явление. Следите, чтобы количество вызовов ::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, должен быть создан заранее. Если системных ресурсов для этого
недостаточно, система сгенерирует исключение, и программа может
"урезать" свою функциональность. Или совсем завершить работу.
ПРИМЕЧАНИЕ
Все это верно для Windows NT/2k/XP. В
Windows 9x/Me используется только поле LockCount. Там находится указатель на
объект ядра, возможно, просто взаимоисключение (mutex). Все остальные поля
равны нулю.
|
API для работы с критическими секциями
BOOL
InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
BOOL
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount);
Заполняют поля структуры, адресуемой
lpCriticalSection. После вызова любой из этих функций критическая секция готова
к работе.
Листинг 1. Псевдокод RtlInitializeCriticalSection из
ntdll.dll
VOID
RtlInitializeCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
RtlInitializeCriticalSectionAndSpinCount(pcs, 0)
}
VOID
RtlInitializeCriticalSectionAndSpinCount(
LPRTL_CRITICAL_SECTION pcs, DWORD dwSpinCount)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
pcs->LockSemaphore = NULL;
pcs->SpinCount = dwSpinCount;
if (0x80000000
& dwSpinCount)
_CriticalSectionGetEvent(pcs);
}
|
DWORD
SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD
dwSpinCount);
Устанавливает значение поля SpinCount и возвращает его
предыдущее значение. Напоминаю, что старший бит отвечает за
"привязку" события, используемого для ожидания доступа к данной
критической секции.
Листинг 2. Псевдокод RtlSetCriticalSectionSpinCount из
ntdll.dll
DWORD
RtlSetCriticalSectionSpinCount(
LPRTL_CRITICAL_SECTION
pcs, DWORD dwSpinCount)
{
DWORD dwRet =
pcs->SpinCount;
pcs->SpinCount =
dwSpinCount;
return dwRet;
}
|
VOID
DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Освобождает ресурсы, занимаемые критической секцией.
Листинг 3. Псевдокод RtlDeleteCriticalSection из
ntdll.dll
VOID
RtlDeleteCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
pcs->DebugInfo = NULL;
pcs->LockCount = -1;
pcs->RecursionCount = 0;
pcs->OwningThread = 0;
if
(pcs->LockSemaphore)
{
::CloseHandle(pcs->LockSemaphore);
pcs->LockSemaphore = NULL;
}
}
|
VOID
EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
BOOL
TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Осуществляют "захват" критической секции.
Если критическая секция занята другой нитью, то ::EnterCriticalSection() будет
ждать, пока та освободится, а ::TryEnterCriticalSection() вернет FALSE.
Отсутствует в Windows 9x/ME.
Листинг 4. Псевдокод RtlEnterCriticalSection из
ntdll.dll
VOID
RtlEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
if
(::InterlockedIncrement(&pcs->LockCount))
{
if
(pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
pcs->RecursionCount++;
return;
}
RtlpWaitForCriticalSection(pcs);
}
pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
BOOL
RtlTryEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)
{
if (-1L ==
::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
{
pcs->OwningThread = (HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
else if
(pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
::InterlockedIncrement(&pcs->LockCount);
pcs->RecursionCount++;
}
else
return
FALSE;
return TRUE;
}
|
VOID
LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Освобождает критическую секцию,
Листинг 5. Псевдокод RtlLeaveCriticalSection из
ntdll.dll
VOID
RtlLeaveCriticalSectionDbg(LPRTL_CRITICAL_SECTION pcs)
{
if
(--pcs->RecursionCount)
::InterlockedDecrement(&pcs->LockCount);
else if
(::InterlockedDecrement(&pcs->LockCount) >= 0)
RtlpUnWaitCriticalSection(pcs);
}
|
Классы-обертки для критических секций
Листинг 6. Код классов CLock, CAutoLock и CScopeLock.
class CLock
{
friend class
CScopeLock;
CRITICAL_SECTION m_CS;
public:
void Init() {
::InitializeCriticalSection(&m_CS); }
void Term() {
::DeleteCriticalSection(&m_CS); }
void Lock() {
::EnterCriticalSection(&m_CS); }
BOOL TryLock()
{ return ::TryEnterCriticalSection(&m_CS); }
void Unlock()
{ ::LeaveCriticalSection(&m_CS); }
};
class CAutoLock
: public CLock
{
public:
CAutoLock() {
Init(); }
~CAutoLock() {
Term(); }
};
class CScopeLock
{
LPCRITICAL_SECTION m_pCS;
public:
CScopeLock(LPCRITICAL_SECTION
pCS) : m_pCS(pCS) { Lock(); }
CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
~CScopeLock()
{ Unlock(); }
void Lock() {
::EnterCriticalSection(m_pCS); }
void Unlock()
{ ::LeaveCriticalSection(m_pCS); }
};
|
Классы CLock и CAutoLock удобно использовать для
синхронизации доступа к переменным класса, а CScopeLock предназначен, в
основном, для использования в процедурах. Удобно, что компилятор сам
позаботится о вызове ::LeaveCriticalSection() через деструктор.
Листинг 7. Пример использования CScopeLock.
CAutoLock m_lockObject;
CObject
*m_pObject;
void Proc1()
{
CScopeLock
lock(m_ lockObject); // Вызов lock.Lock();
if
(!m_pObject)
return; // Вызов
lock.Unlock();
m_pObject->SomeMethod();
// Вызов
lock.Unlock();
}
|
Отладка критических секций
Весьма интересное и увлекательное занятие. Можно
потратить часы и недели, но так и не найти, где именно возникает проблема.
Стоит уделить этому особо пристальное внимание. Ошибки, связанные с
критическими секциями, бывают двух типов: ошибки реализации и архитектурные
ошибки.
Ошибки, связанные с реализацией
Это довольно легко обнаруживаемые ошибки, как правило,
связанные с непарностью вызовов ::EnterCriticalSection() и
::LeaveCriticalSection().
Листинг 8. Пропущен вызов
::EnterCriticalSection().
// Процедура предполагает, что
m_lockObject.Lock(); уже был вызван
void Pool()
{
for (int i = 0; i <
m_vectSinks.size(); i++)
{
m_lockObject.Unlock();
m_vectSinks[i]->DoSomething();
m_lockObject.Lock();
}
}
|
::LeaveCriticalSection() без ::EnterCriticalSection()
приведет к тому, что первый же вызов ::EnterCriticalSection() остановит
выполнение нити навсегда.
Листинг 9. Пропущен вызов
::LeaveCriticalSection().
void Proc()
{
m_lockObject.Lock();
if (!m_pObject)
return;
//. ..
m_lockObject.Unlock();
}
|
В этом примере, конечно, имеет смысл воспользоваться
классом типа 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), когда две нити пытаются захватить две или более критических секций,
причем делают это в разном порядке.
Листинг 10. Взаимоблокировка двух ниток.
void Proc1()
// Нить
№1
{
::EnterCriticalSection(&m_lock1);
//. ..
::EnterCriticalSection(&m_lock2);
//. ..
::LeaveCriticalSection(&m_lock2);
//. ..
::LeaveCriticalSection(&m_lock1);
}
// Нить
№2
void Proc2()
{
::EnterCriticalSection(&m_lock2);
//. ..
::EnterCriticalSection(&m_lock1);
//. ..
::LeaveCriticalSection(&m_lock1);
//. ..
::LeaveCriticalSection(&m_lock2);
}
|
Проблемы могут возникнуть и при... копировании
критических секций. Понятно, что вот такой код вряд ли сможет написать
программист в здравом уме и памяти:
CRITICAL_SECTION
sec1;
CRITICAL_SECTION
sec2;
//. ..
sec1 = sec2;
|
Из такого присвоения трудно извлечь какую-либо пользу.
А вот такой код иногда пишут:
struct SData
{
CLock m_lock;
DWORD
m_dwSmth;
} m_data;
void
Proc1(SData& data)
{
m_data = data;
}
|
и все бы хорошо, если бы у структуры SData был конструктор
копирования, например такой:
SData(const SData data)
{
CScopeLock
lock(data.m_lock);
m_dwSmth = data.m_dwSmth;
}
|
Но нет, программист посчитал, что хватит за глаза
простого копирования полей, и, в результате, переменная m_lock была просто
скопирована, хотя именно в этот момент из другой нити она была
"захвачена", и значение поля LockCount у нее в этот момент больше
либо равно нулю. После вызова ::LeaveCriticalSection() в той нити, у исходной
переменной m_lock значение поля LockCount уменьшилось на единицу. А у
скопированной переменной – осталось прежним. И любой вызов
::EnterCriticalSection() в этой нити никогда не вернется. Он будет вечно ждать
неизвестно чего.
Это только цветочки. С ягодками вы очень быстро
столкнетесь, если попытаетесь написать что-нибудь действительно сложное.
Например, ActiveX-объект в многопоточном подразделении (MTA), создаваемый из
скрипта, запущенного из-под контейнера, размещенного в однопоточном
подразделении (STA). Ни слова не понятно? Не беда. Сейчас я попытаюсь выразить
проблему более понятным языком. Итак. Имеется объект, вызывающий методы другого
объекта, причем живут они в разных нитях. Вызовы производятся синхронно. Т.е.
объект №1 переключает выполнение на нить объекта №2, вызывает метод и
переключается обратно на свою нить. При этом выполнение нити №1 приостановлено
до тех пор, пока не отработает нить объекта №2. Теперь, положим, объект №2
вызывает метод объекта №1 из своей нити. Получается, что управление вернулось в
объект №1, но из нити объекта №2. Если объект №1 вызывал метод объекта №2,
захватив какую-либо критическую секцию, то при вызове метода объекта №1 тот
заблокирует сам себя при повторном входе в ту же критическую секцию.
Листинг 11. Самоблокировка средствами одного объекта.
// Нить №1
void IObject1::Proc1()
{
// Входим в критическую
секцию объекта №1
m_lockObject.Lock();
// Вызываем метод объекта
№2, происходит переключение на нить объекта №2
m_pObject2->SomeMethod();
// Сюда мы попадем только
по возвращении из m_pObject2->SomeMethod()
m_lockObject.Unlock();
}
// Нить
№2
void
IObject2::SomeMethod()
{
// Вызываем метод объекта
№1 из нити объекта №2
m_pObject1->Proc2();
}
// Нить
№2
void
IObject1::Proc2()
{
// Пытаемся войти в
критическую секцию объекта №1
m_lockObject.Lock();
// Сюда мы не попадем
никогда
m_lockObject.Unlock();
}
|
Если бы в примере не было переключения нитей, все
вызовы произошли бы в нити объекта №1, и никаких проблем не возникло. Сильно
надуманный пример? Ничуть. Именно переключение ниток лежит в основе подразделений
(apartments) COM. А из этого следует одно очень, очень неприятное правило.
СОВЕТ
Избегайте вызовов каких бы то ни было
объектов при захваченных критических секциях.
|
Помните пример из начала статьи? Так вот, он абсолютно
неприемлем в подобных случаях. Его придется переделать на что-то вроде примера,
приведенного в листинге 12.
Листинг 12. Простой пример, не подверженный
самоблокировке.
// Нить
№1
void Proc1()
{
m_lockObject.Lock();
CComPtr<IObject> pObject(m_pObject); // вызов
pObject->AddRef();
m_lockObject.Unlock();
if (pObject)
pObject->SomeMethod();
}
// Нить
№2
void
Proc2(IObject *pNewObject)
{
m_lockObject.Lock();
m_pObject =
pNewobject;
m_lockObject.Unlock();
}
|
Доступ к объекту по-прежнему синхронизован, но вызов
SomeMethod(); происходит вне критической секции. Победа? Почти. Осталась одна
маленькая деталь. Давайте посмотрим, что происходит в Proc2():
void
Proc2(IObject *pNewObject)
{
m_lockObject.Lock();
if
(m_pObject.p)
m_pObject.p->Release();
m_pObject.p =
pNewobject;
if
(m_pObject.p)
m_pObject.p->AddRef();
m_lockObject.Unlock();
}
|
Очевидно, что вызовы m_pObject.p->AddRef(); и
m_pObject.p->Release(); происходят внутри критической секции. И если вызов
метода AddRef(), как правило, безвреден, то вызов метода Release() может
оказаться последним вызовом Release(), и объект самоуничтожится. В методе
FinalRelease() объекта №2 может быть все что угодно, например, освобождение
объектов, живущих в других подразделениях. А это опять приведет к переключению
ниток и может вызвать самоблокировку объекта №1 по уже известному сценарию.
Придется воспользоваться той же техникой, что и в методе Proc1():
Листинг 13
// Нить
№2
void
Proc2(IObject *pNewObject)
{
m_lockObject.Lock();
pPrevObject.Attach(m_pObject.Detach());
m_pObject =
pNewobject;
m_lockObject.Unlock();
// pPrevObject.Release();
}
|
Теперь потенциально последний вызов
IObject2::Release() будет осуществлен после выхода из критической секции. А
присвоение нового значения по-прежнему синхронизовано с вызовом
IObject2::SomeMethod() из нити №1.
Способы обнаружения ошибок
Сначала стоит обратить внимание на
"официальный" способ обнаружения блокировок. Если бы кроме
::EnterCriticalSection() и ::TryEnterCtiticalSection() существовал еще и
::EnterCriticalSectionWithTimeout(), то достаточно было бы просто указать
какое-нибудь резонное значение для интервала ожидания, например, 30 секунд.
Если критическая секция не освободилась в течение указанного времени, то с очень
большой вероятностью она не освободится никогда. Имеет смысл подключить
отладчик и посмотреть, что же творится в соседних нитях. Но увы. Никаких
::EnterCriticalSectionWithTimeout() в Win32 не предусмотрено. Вместо этого есть
поле CriticalSectionDefaultTimeout в структуре IMAGE_LOAD_CONFIG_DIRECTORY32,
которое всегда равно нулю и, судя по всему, не используется. Зато используется
ключ в реестре "HKLM\SYSTEM\CurrentControlSet\Control\Session
Manager\CriticalSectionTimeout", который по умолчанию равен 30 суткам, и
по истечению этого времени в системный лог попадает строка "RTL: Enter
Critical Section Timeout (2 minutes)\nRTL: Pid.Tid XXXX.YYYY, owner tid
ZZZZ\nRTL: Re-Waiting\n". К тому же это верно только для систем
WindowsNT/2k/XP и только с CheckedBuild. У вас установлен CheckedBuild? Нет? А
зря. Вы теряете исключительную возможность увидеть эту замечательную строку.
Ну, а какие у нас альтернативы? Да, пожалуй, только
одна. Не использовать API для работы с критическими секциями. Вместо них
написать свои собственные. Пусть даже не такие обточенные напильником, как в
Windows NT. Не страшно. Нам это понадобится только в debug-конфигурациях. В
release'ах мы будем продолжать использовать оригинальный API от Майкрософт. Для
этого напишем несколько функций, полностью совместимых по типам и количеству
аргументов с "настоящим" API, и добавим #define, как у MFC, для
переопределения оператора new в debug-конфигурациях.
Листинг 14. Собственная реализация критических секций.
#if
defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)
#define
DEADLOCK_TIMEOUT 30000
#define CS_DEBUG
1
// Создаем на лету событие
для операций ожидания,
// но никогда его не
освобождаем. Так удобней для отладки
static inline
HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs)
{
HANDLE ret =
pcs->LockSemaphore;
if (!ret)
{
HANDLE sem =
::CreateEvent(NULL, false, false, NULL);
ATLASSERT(sem);
if (!(ret =
(HANDLE)::InterlockedCompareExchangePointer(
&pcs->LockSemaphore, sem, NULL)))
ret = sem;
else
::CloseHandle(sem);
// Кто-то успел раньше
}
return ret;
}
// Ждем, пока критическая
секция не освободится либо время ожидания
// будет превышено
static inline
VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
HANDLE sem =
_CriticalSectionGetEvent(pcs);
DWORD dwWait;
do
{
dwWait =
::WaitForSingleObject(sem, DEADLOCK_TIMEOUT);
if
(WAIT_TIMEOUT == dwWait)
{
ATLTRACE("Critical section timeout (%u msec):"
"
tid 0x%04X owner tid 0x%04X\n", DEADLOCK_TIMEOUT,
::GetCurrentThreadId(), pcs->OwningThread);
}
}while(WAIT_TIMEOUT == dwWait);
ATLASSERT(WAIT_OBJECT_0 == dwWait);
}
// Выставляем событие в
активное состояние
static inline VOID
_UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
HANDLE sem =
_CriticalSectionGetEvent(pcs);
BOOL b =
::SetEvent(sem);
ATLASSERT(b);
}
// Заполучаем критическую
секцию в свое пользование
inline VOID
EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
if
(::InterlockedIncrement(&pcs->LockCount))
{
// LockCount
стал
больше нуля.
// Проверяем идентификатор нити
if
(pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
// Нить та же самая.
Критическая секция наша.
pcs->RecursionCount++;
return;
}
// Критическая секция
занята другой нитью.
// Придется подождать
_WaitForCriticalSectionDbg(pcs);
}
// Либо критическая
секция была "свободна",
// либо мы дождались.
Сохраняем идентификатор текущей нити.
pcs->OwningThread =
(HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
// Заполучаем критическую
секцию, если она никем не занята
inline BOOL
TryEnterCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
if (-1L ==
::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
{
// Это первое обращение
к критической секции
pcs->OwningThread =
(HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
}
else if
(pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
// Это не первое
обращение, но из той же нити
::InterlockedIncrement(&pcs->LockCount);
pcs->RecursionCount++;
}
else
return FALSE; //
Критическая секция занята другой нитью
return TRUE;
}
// Освобождаем критическую секцию
inline VOID
LeaveCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
// Проверяем, чтобы
идентификатор текущей нити совпадал
// с идентификатором
нити-владельца.
// Если это не так,
скорее всего мы имеем дело с ошибкой
ATLASSERT(pcs->OwningThread ==
(HANDLE)::GetCurrentThreadId());
if (--pcs->RecursionCount)
{
// Не последний вызов
из этой нити.
// Уменьшаем значение
поля LockCount
::InterlockedDecrement(&pcs->LockCount);
}
else
{
// Последний вызов.
Нужно "разбудить" какую-либо
// из ожидающих ниток,
если таковые имеются
ATLASSERT(NULL != pcs->OwningThread);
pcs->OwningThread = NULL;
if
(::InterlockedDecrement(&pcs->LockCount) >= 0)
{
// Имеется, как минимум, одна ожидающая нить
_UnWaitCriticalSectionDbg(pcs);
}
}
}
// Удостоверяемся, что ::EnterCriticalSection() была вызвана
// до вызова этого метода
inline BOOL
CheckCriticalSection(LPCRITICAL_SECTION pcs)
{
return
pcs->LockCount >= 0
&&
pcs->OwningThread == (HANDLE)::GetCurrentThreadId();
}
// Переопределяем все
функции для работы с критическими секциями.
// Определение класса CLock
должно быть после этих строк
#define
EnterCriticalSection EnterCriticalSectionDbg
#define
TryEnterCriticalSection TryEnterCriticalSectionDbg
#define
LeaveCriticalSection LeaveCriticalSectionDbg
#endif
|
Ну и заодно добавим еще один метод в наш класс Clock
(листинг 15).
Листинг 15. Класс CLock с новым методом.
class CLock
{
friend class
CScopeLock;
CRITICAL_SECTION m_CS;
public:
void Init() {
::InitializeCriticalSection(&m_CS); }
void Term() {
::DeleteCriticalSection(&m_CS); }
void Lock() {
::EnterCriticalSection(&m_CS); }
BOOL TryLock()
{ return ::TryEnterCriticalSection(&m_CS); }
void Unlock()
{ ::LeaveCriticalSection(&m_CS); }
BOOL Check() {
return CheckCriticalSection(&m_CS); }
};
|
Использовать метод Check() в release-конфигурациях не
стоит, возможно, что в будущем, в какой-нибудь Windows64, структура
RTL_CRITICAL_SECTION изменится, и результат такой проверки будет не определен.
Так что ему самое место "жить" внутри всяческих ASSERT'ов.
Итак, что мы имеем? Мы имеем проверку на лишний вызов
::LeaveCriticalSection() и ту же трассировку для блокировок. Не так уж много.
Особенно если трассировка о блокировке имеет место, а вот нить, забывшая
освободить критическую секцию, давно завершилась. Как быть? Вернее, что бы еще
придумать, чтобы ошибку проще было выявить? Как минимум, прикрутить сюда
__LINE__ и __FILE__, константы, соответствующие текущей строке и имени файла на
момент компиляции этого метода.
VOID
EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs
, int nLine = __LINE__,
azFile = __FILE__);
|
Компилируем, запускаем... Результат удивительный. Хотя
правильный. Компилятор честно подставил номер строки и имя файла,
соответствующие началу нашей EnterCriticalSectionDbg(). Так что придется
попотеть немного больше. __LINE__ и __FILE__ нужно вставить в #define'ы, тогда
мы получим действительные номер строки и имя исходного файла. Теперь вопрос,
куда же сохранить эти параметры для дальнейшего использования? Причем хочется
оставить за собой возможность вызова стандартных функций API наряду с нашими
собственными? На помощь приходит C++: просто создадим свою структуру,
унаследовав ее от RTL_CRITICAL_SECTION (листинг 16).
Листинг 16. Реализация критических секций с
сохранением строки и имени файла.
#if
defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)
#define
DEADLOCK_TIMEOUT 30000
#define CS_DEBUG
2
// Наша структура взамен
CRITICAL_SECTION
struct
CRITICAL_SECTION_DBG : public CRITICAL_SECTION
{
// Добавочные поля
int m_nLine;
LPCSTR m_azFile;
};
typedef struct
CRITICAL_SECTION_DBG *LPCRITICAL_SECTION_DBG;
// Создаем на лету событие
для операций ожидания,
// но никогда его не
освобождаем. Так удобней для отладки.
static inline
HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs)
{
HANDLE ret =
pcs->LockSemaphore;
if (!ret)
{
HANDLE sem =
::CreateEvent(NULL, false, false, NULL);
ATLASSERT(sem);
if (!(ret =
(HANDLE)::InterlockedCompareExchangePointer(
&pcs->LockSemaphore, sem, NULL)))
ret = sem;
else
::CloseHandle(sem);
// Кто-то успел раньше
}
return ret;
}
// Ждем, пока критическая
секция не освободится либо время ожидания
// будет превышено
static inline
VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
, int nLine,
LPCSTR azFile)
{
HANDLE sem =
_CriticalSectionGetEvent(pcs);
DWORD dwWait;
do
{
dwWait =
::WaitForSingleObject(sem, DEADLOCK_TIMEOUT);
if
(WAIT_TIMEOUT == dwWait)
{
ATLTRACE("Critical section timeout (%u msec):"
"
tid 0x%04X owner tid 0x%04X\n"
"Owner lock from %hs line %u, waiter %hs line %u\n"
,
DEADLOCK_TIMEOUT
,
::GetCurrentThreadId(), pcs->OwningThread
,
pcs->m_azFile, pcs->m_nLine, azFile, nLine);
}
}while(WAIT_TIMEOUT == dwWait);
ATLASSERT(WAIT_OBJECT_0 == dwWait);
}
// Выставляем событие в активное состояние
static inline
VOID _UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs)
{
HANDLE sem =
_CriticalSectionGetEvent(pcs);
BOOL b =
::SetEvent(sem);
ATLASSERT(b);
}
// Инициализируем
критическую секцию.
inline VOID
InitializeCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
// Пусть система заполнит
свои поля
InitializeCriticalSection(pcs);
// Заполняем наши поля
pcs->m_nLine = 0;
pcs->m_azFile = NULL;
}
// Освобождаем ресурсы,
занимаемые критической секцией
inline VOID
DeleteCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
// Проверяем, чтобы не
было удалений "захваченных" критических секций
ATLASSERT(0 == pcs->m_nLine && NULL ==
pcs->m_azFile);
// Остальное доделает система
DeleteCriticalSection(pcs);
}
// Заполучаем критическую
секцию в свое пользование
inline VOID
EnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
, int nLine,
LPSTR azFile)
{
if
(::InterlockedIncrement(&pcs->LockCount))
{
// LockCount стал
больше нуля.
// Проверяем
идентификатор нити
if (pcs->OwningThread ==
(HANDLE)::GetCurrentThreadId())
{
// Нить та же самая.
Критическая секция наша.
// Никаких
дополнительных действий не производим.
// Это не совсем
верно, так как возможно, что непарный
// вызов
::LeaveCriticalSection() был сделан на n-ном заходе,
// и это придется
отлавливать вручную, но реализация
// стека для __LINE__
и __FILE__ сделает нашу систему
// более громоздкой.
Если это действительно необходимо,
// вы всегда можете
сделать это самостоятельно
pcs->RecursionCount++;
return;
}
// Критическая секция
занята другой нитью.
// Придется подождать
_WaitForCriticalSectionDbg(pcs, nLine, azFile);
}
// Либо критическая
секция была "свободна",
// либо мы дождались.
Сохраняем идентификатор текущей нити.
pcs->OwningThread =
(HANDLE)::GetCurrentThreadId();
pcs->RecursionCount
= 1;
pcs->m_nLine = nLine;
pcs->m_azFile = azFile;
}
// Заполучаем критическую
секцию, если она никем не занята
inline BOOL
TryEnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs
, int nLine,
LPSTR azFile)
{
if (-1L ==
::InterlockedCompareExchange(&pcs->LockCount, 0, -1))
{
// Это первое обращение
к критической секции
pcs->OwningThread =
(HANDLE)::GetCurrentThreadId();
pcs->RecursionCount = 1;
pcs->m_nLine = nLine;
pcs->m_azFile = azFile;
}
else if
(pcs->OwningThread == (HANDLE)::GetCurrentThreadId())
{
// Это не первое
обращение, но из той же нити
::InterlockedIncrement(&pcs->LockCount);
pcs->RecursionCount++;
}
else
return FALSE; //
Критическая секция занята другой нитью
return TRUE;
}
// Освобождаем критическую секцию
inline VOID
LeaveCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs)
{
// Проверяем, чтобы
идентификатор текущей нити совпадал
// с идентификатором
нити-влядельца.
// Если это не так,
скорее всего мы имеем дело с ошибкой
ATLASSERT(pcs->OwningThread ==
(HANDLE)::GetCurrentThreadId());
if (--pcs->RecursionCount)
{
// Не последний вызов
из этой нити.
// Уменьшаем значение
поля LockCount
::InterlockedDecrement(&pcs->LockCount);
}
else
{
// Последний вызов.
Нужно "разбудить" какую-либо
// из ожидающих ниток,
если таковые имеются
ATLASSERT(NULL != pcs->OwningThread);
pcs->OwningThread = NULL;
pcs->m_nLine = 0;
pcs->m_azFile = NULL;
if
(::InterlockedDecrement(&pcs->LockCount) >= 0)
{
// Имеется, как минимум, одна ожидающая нить
_UnWaitCriticalSectionDbg(pcs);
}
}
}
// Удостоверяемся, что ::EnterCriticalSection() была вызвана
// до вызова этого метода
inline BOOL
CheckCriticalSection(LPCRITICAL_SECTION pcs)
{
return
pcs->LockCount >= 0
&&
pcs->OwningThread == (HANDLE)::GetCurrentThreadId();
}
// Переопределяем все
функции для работы с критическими секциями.
// Определение класса CLock
должно быть после этих строк
#define
InitializeCriticalSection InitializeCriticalSectionDbg
#define
InitializeCriticalSectionAndSpinCount(pcs, c) \
InitializeCriticalSectionDbg(pcs)
#define
DeleteCriticalSection DeleteCriticalSectionDbg
#define
EnterCriticalSection(pcs) EnterCriticalSectionDbg(pcs, __LINE__, __FILE__)
#define
TryEnterCriticalSection(pcs) \
TryEnterCriticalSectionDbg(pcs, __LINE__, __FILE__)
#define
LeaveCriticalSection LeaveCriticalSectionDbg
#define
CRITICAL_SECTION CRITICAL_SECTION_DBG
#define LPCRITICAL_SECTION
LPCRITICAL_SECTION_DBG
#define
PCRITICAL_SECTION PCRITICAL_SECTION_DBG
#endif
|
Приводим наши классы в соответствие (листинг 17).
Листинг 17. Классы CLock и CScopeLock, вариант для
отладки.
class CLock
{
friend class
CScopeLock;
CRITICAL_SECTION m_CS;
public:
void Init() {
::InitializeCriticalSection(&m_CS); }
void Term() {
::DeleteCriticalSection(&m_CS); }
#if
defined(CS_DEBUG)
BOOL Check() {
return CheckCriticalSection(&m_CS); }
#endif
#if CS_DEBUG
> 1
void Lock(int
nLine, LPSTR azFile) { EnterCriticalSectionDbg(&m_CS, nLine, azFile); }
BOOL
TryLock(int nLine, LPSTR azFile) { return
TryEnterCriticalSectionDbg(&m_CS, nLine, azFile); }
#else
void Lock() {
::EnterCriticalSection(&m_CS); }
#endif
void Unlock()
{ ::LeaveCriticalSection(&m_CS); }
};
class CScopeLock
{
LPCRITICAL_SECTION m_pCS;
public:
#if CS_DEBUG
> 1
CScopeLock(LPCRITICAL_SECTION pCS, int nLine, LPSTR azFile) : m_pCS(pCS) {
Lock(nLine, azFile); }
CScopeLock(CLock& lock, int nLine, LPSTR azFile) : m_pCS(&lock.m_CS)
{ Lock(nLine, azFile); }
void Lock(int
nLine, LPSTR azFile) { EnterCriticalSectionDbg(m_pCS, nLine, azFile); }
#else
CScopeLock(LPCRITICAL_SECTION pCS) : m_pCS(pCS) { Lock(); }
CScopeLock(CLock& lock) : m_pCS(&lock.m_CS) { Lock(); }
void Lock() {
::EnterCriticalSection(m_pCS); }
#endif
~CScopeLock()
{ Unlock(); }
void Unlock()
{ ::LeaveCriticalSection(m_pCS); }
};
#if CS_DEBUG
> 1
#define Lock()
Lock(__LINE__, __FILE__)
#define
TryLock() TryLock(__LINE__, __FILE__)
#define lock(cs)
lock(cs, __LINE__, __FILE__)
#endif
|
К сожалению, пришлось даже переопределить CScopeLock
lock(cs), причем жестко привязаться к имени переменной. Не стоит говорить о
том, что наверняка получился конфликт имен - все-таки Lock довольно популярное
название для метода. Такой код не будет собираться, например, с популярнейшей
библиотекой ATL. Тут есть два способа. Переименовать методы Lock() и TryLock()
во что-нибудь более уникальное, либо переименовать Lock() в ATL:
// StdAfx.h
//. ..
#define Lock
ATLLock
#include
<AtlBase.h>
//. ..
|
Сменим тему
А что это мы все про Win32 API да про C++? Давайте
посмотрим, как обстоят дела с критическими секциями в более современных языках
программирования.
C#
Тут стараниями Майкрософт имеется полный набор старого
доброго API под новыми именами.
Критические секции представлены классом
System.Threading.Monitor, вместо ::EnterCriticalSection() есть
Monitor.Enter(object), а вместо ::LeaveCriticalSection() Monitor.Exit(object),
где object – это любой объект C#. Т.е. каждый объект где-то в потрохах CLR
(Common Language Runtime) имеет свою собственную критическую секцию либо
заводит ее по необходимости. Типичное использование этой секции выглядит так:
Monitor.Enter(this);
m_dwSmth = dwSmth;
Monitor.Exit(this);
|
Если нужно организовать отдельную критическую секцию
для какой-либо переменной, самым логичным способом будет поместить ее в
отдельный объект и использовать этот объект как аргумент при вызове
Monitor.Enter/Exit(). Кроме того, в C# существует ключевое слово lock, это
полный аналог нашего класса CScopeLock.
lock (this)
{
m_dwSmth = dwSmth;
}
|
А вот Monitor.TryEnter() в C# (о, чудо!) принимает в
качестве параметра максимальный период ожидания.
Замечу, что CLR – это не только C#, все это применимо
и к другим языкам, использующим CLR.
Java
В этом языке используется подобный механизм, только
место ключевого слова lock есть ключевое слово synchronized, а все остальное –
точно так же.
synchronized (this)
{
m_dwSmth = dwSmth;
}
|
MC++ (управляемый C++)
Тут тоже появился атрибут [synchronized] ведущий себя
точно так же, как и одноименное ключевое слово из Java. Странно, что
архитекторы из Майкрософт решили позаимствовать синтаксис из продукта от Sun
Microsystems вместо своего собственного.
[synchronized] DWORD
m_dwSmth;
//...
m_dwSmth = dwSmth; // неявный вызов
Lock(this)
|
Delphi
Практически все, что верно для C++, верно и для
Delphi. Критические секции представлены объектом TCriticalSection. Собственно,
это такая же обертка как и наш класс CLock.
Кроме того, в Delphi присутствует специальный объект
TMultiReadExclusiveWriteSynchronizer с названием, говорящим само за себя.
Подведем итоги
Итак, что нужно знать о критических секциях:
Критические секции работают быстро и не требуют
большого количества системных ресурсов.
Для синхронизации доступа к нескольким (независимым)
переменным лучше использовать несколько критических секций, а не одну для всех.
Код, ограниченный критическими секциями, лучше всего
свести к минимуму.
Находясь в критической секции, не стоит вызывать
методы "чужих" объектов.
Список литературы
Для подготовки данной работы были использованы
материалы с сайта http://www.rsdn.ru/