Смекни!
smekni.com

Критические секции (стр. 2 из 4)

Осуществляют "захват" критической секции. Если критическая секция занята другой нитью, то ::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);}// Нить №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():