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

39504
знака
37
таблиц
0
изображений

Павел Блудов

Введение

Критические секции -- это объекты, используемые для блокировки доступа всех нитей (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 l(m_lock);

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

CScopeLock (m_lock);

Что это означает? Компилятор честно сделал вызов конструктора 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)

{

CComPtr<IObject> pPrevObject;

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, которое всегда равно нулю и, судя по всему, не используется. Зато используется ключ в реестре "HKLMSYSTEMCurrentControlSetControlSession ManagerCriticalSectionTimeout", который по умолчанию равен 30 суткам, и по истечению этого времени в системный лог попадает строка "RTL: Enter Critical Section Timeout (2 minutes)nRTL: Pid.Tid XXXX.YYYY, owner tid ZZZZnRTL: Re-Waitingn". К тому же это верно только для систем 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%04Xn", 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%04Xn"

"Owner lock from %hs line %u, waiter %hs line %un"

, 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); }

BOOL TryLock() { return ::TryEnterCriticalSection(&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 с названием, говорящим само за себя.

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

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

Критические секции работают быстро и не требуют большого количества системных ресурсов.

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

Код, ограниченный критическими секциями, лучше всего свести к минимуму.

Находясь в критической секции, не стоит вызывать методы "чужих" объект


Информация о работе «Критические секции»
Раздел: Информатика, программирование
Количество знаков с пробелами: 39504
Количество таблиц: 37
Количество изображений: 0

Похожие работы

Скачать
16857
1
3

... , чем централизованный), а в третьем - потеря токена или отказ процесса. Рис. 3.7. Средства взаимного исключения в распределенных системах а - неупорядоченная группа процессов в сети; б - логическое кольцо, образованное программным обеспечением Неделимые транзакции Все средства синхронизации, которые были рассмотрены ранее, относятся к нижнему уровню, например, семафоры. Они требуют от ...

Скачать
18440
1
0

... (другими объектами mutex, семафорами, событиями и прочим). Об этом — подробнее в последующих разделах. События События, как и объекты исключительного владения, могут использоваться для синхронизации потоков, принадлежащих разным приложениям. Самые значительные отличия сводятся к следующему: ·   событиями никто не владеет — то есть устанавливать события в свободное или занятое состояние ...

Скачать
155611
5
0

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

Скачать
127060
2
1

... для таблиц dBASE и Paradox. С использованием этих компонентов создание программы просмотра и редактирования базы данных почти не требует программирования. Win 3.1. На этой странице находятся компоненты Delphi 1.0, возможности которых перекрываются аналогичными компонентами Windows 95. Internet. Эта страница предоставляет компоненты для разработки приложений, позволяющих создавать HTML ...

0 комментариев


Наверх