РефератыИнформатика, программированиеКрКритические секции

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

Павел Блудов


Введение


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


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


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


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


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


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


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

Сохранить в соц. сетях:
Обсуждение:
comments powered by Disqus

Название реферата: Критические секции

Слов:5199
Символов:52622
Размер:102.78 Кб.