Осуществляют "захват" критической секции. Если критическая секция занята другой нитью, то ::EnterCriticalSection() будет ждать, пока та освободится, а ::TryEnterCriticalSection() вернет FALSE. Отсутствует в Windows 9x/ME.
Листинг 4. Псевдокод RtlEnterCriticalSection из ntdll.dllVOID 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.dllVOID 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);}// Нить №2void 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. Самоблокировка средствами одного объекта.
// Нить №1void IObject1::Proc1(){ // Входим в критическую секцию объекта №1 m_lockObject.Lock(); // Вызываем метод объекта №2, происходит переключение на нить объекта №2 m_pObject2->SomeMethod(); // Сюда мы попадем только по возвращении из m_pObject2->SomeMethod()m_lockObject.Unlock();}// Нить №2void IObject2::SomeMethod(){ // Вызываем метод объекта №1 из нити объекта №2m_pObject1->Proc2();}// Нить №2void IObject1::Proc2(){ // Пытаемся войти в критическую секцию объекта №1 m_lockObject.Lock(); // Сюда мы не попадем никогда m_lockObject.Unlock();} |
Если бы в примере не было переключения нитей, все вызовы произошли бы в нити объекта №1, и никаких проблем не возникло. Сильно надуманный пример? Ничуть. Именно переключение ниток лежит в основе подразделений (apartments) COM. А из этого следует одно очень, очень неприятное правило.
СОВЕТИзбегайте вызовов каких бы то ни было объектов при захваченных критических секциях. |
Помните пример из начала статьи? Так вот, он абсолютно неприемлем в подобных случаях. Его придется переделать на что-то вроде примера, приведенного в листинге 12.
Листинг 12. Простой пример, не подверженный самоблокировке.
// Нить №1void Proc1(){ m_lockObject.Lock(); CComPtr<IObject> pObject(m_pObject); // вызов pObject->AddRef(); m_lockObject.Unlock(); if (pObject) pObject->SomeMethod();}// Нить №2void 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():