Смекни!
smekni.com

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

Листинг 13

// Нить №2void 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, которое всегда равно нулю и, судя по всему, не используется. Зато используется ключ в реестре "HKLM&bsol;SYSTEM&bsol;CurrentControlSet&bsol;Control&bsol;Session Manager&bsol;CriticalSectionTimeout", который по умолчанию равен 30 суткам, и по истечению этого времени в системный лог попадает строка "RTL: Enter Critical Section Timeout (2 minutes)&bsol;nRTL: Pid.Tid XXXX.YYYY, owner tid ZZZZ&bsol;nRTL: Re-Waiting&bsol;n". К тому же это верно только для систем WindowsNT/2k/XP и только с CheckedBuild. У вас установлен CheckedBuild? Нет? А зря. Вы теряете исключительную возможность увидеть эту замечательную строку.

Ну, а какие у нас альтернативы? Да, пожалуй, только одна. Не использовать API для работы с критическими секциями. Вместо них написать свои собственные. Пусть даже не такие обточенные напильником, как в Windows NT. Не страшно. Нам это понадобится только в debug-конфигурациях. В release'ах мы будем продолжать использовать оригинальный API от Майкрософт. Для этого напишем несколько функций, полностью совместимых по типам и количеству аргументов с "настоящим" API, и добавим #define, как у MFC, для переопределения оператора new в debug-конфигурациях.

Листинг 14. Собственная реализация критических секций.

#if defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)#define DEADLOCK_TIMEOUT 30000#define CS_DEBUG 1// Создаем на лету событие для операций ожидания,// но никогда его не освобождаем. Такудобнейдляотладкиstatic inline HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs){ HANDLE ret = pcs->LockSemaphore; if (!ret) { HANDLE sem = ::CreateEvent(NULL, false, false, NULL); ATLASSERT(sem); if (!(ret = (HANDLE)::InterlockedCompareExchangePointer( &pcs->LockSemaphore, sem, NULL))) ret = sem;else ::CloseHandle(sem); // Кто-то успел раньше } return ret;}// Ждем, пока критическая секция не освободится либо время ожидания// будетпревышеноstatic inline VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION pcs){ HANDLE sem = _CriticalSectionGetEvent(pcs); DWORD dwWait; do { dwWait = ::WaitForSingleObject(sem, DEADLOCK_TIMEOUT); if (WAIT_TIMEOUT == dwWait) { ATLTRACE("Critical section timeout (%u msec):" " tid 0x%04X owner tid 0x%04X&bsol;n", DEADLOCK_TIMEOUT, ::GetCurrentThreadId(), pcs->OwningThread); } }while(WAIT_TIMEOUT == dwWait);ATLASSERT(WAIT_OBJECT_0 == dwWait);}// Выставляем событие в активное состояниеstatic inline VOID _UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs){ HANDLE sem = _CriticalSectionGetEvent(pcs); BOOL b = ::SetEvent(sem);ATLASSERT(b);}// Заполучаем критическую секцию в свое пользованиеinline VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs){ if (::InterlockedIncrement(&pcs->LockCount)) { // LockCount сталбольшенуля. // Проверяемидентификаторнити if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId()){ // Нить та же самая. Критическая секция наша. pcs->RecursionCount++; return; } // Критическая секция занята другой нитью. // Придется подождать _WaitForCriticalSectionDbg(pcs); } // Либо критическая секция была "свободна", // либо мы дождались. Сохраняем идентификатор текущей нити.pcs->OwningThread = (HANDLE)::GetCurrentThreadId(); pcs->RecursionCount = 1;}// Заполучаем критическую секцию, если она никем не занятаinline BOOL TryEnterCriticalSectionDbg(LPCRITICAL_SECTION pcs){ if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1)){ // Это первое обращение к критической секцииpcs->OwningThread = (HANDLE)::GetCurrentThreadId(); pcs->RecursionCount = 1; } else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId()){ // Это не первое обращение, но из той же нити::InterlockedIncrement(&pcs->LockCount); pcs->RecursionCount++;} else return FALSE; // Критическая секция занята другой нитьюreturn TRUE;}// Освобождаемкритическуюсекциюinline VOID LeaveCriticalSectionDbg(LPCRITICAL_SECTION pcs){ // Проверяем, чтобы идентификатор текущей нити совпадал // с идентификатором нити-владельца. // Если это не так, скорее всего мы имеем дело с ошибкойATLASSERT(pcs->OwningThread == (HANDLE)::GetCurrentThreadId());if (--pcs->RecursionCount) { // Не последний вызов из этой нити. // Уменьшаем значение поля LockCount ::InterlockedDecrement(&pcs->LockCount); } else { // Последний вызов. Нужно "разбудить" какую-либо // из ожидающих ниток, если таковые имеютсяATLASSERT(NULL != pcs->OwningThread); pcs->OwningThread = NULL; if (::InterlockedDecrement(&pcs->LockCount) >= 0) { // Имеется, какминимум, однаожидающаянить _UnWaitCriticalSectionDbg(pcs); } }}// Удостоверяемся, что ::EnterCriticalSection() былавызвана// довызоваэтогометодаinline BOOL CheckCriticalSection(LPCRITICAL_SECTION pcs){ return pcs->LockCount >= 0 && pcs->OwningThread == (HANDLE)::GetCurrentThreadId();}// Переопределяем все функции для работы с критическими секциями.// Определение класса CLock должно быть после этих строк#define EnterCriticalSection EnterCriticalSectionDbg#define TryEnterCriticalSection TryEnterCriticalSectionDbg#define LeaveCriticalSection LeaveCriticalSectionDbg#endif

Ну и заодно добавим еще один метод в наш класс Clock (листинг 15).

Листинг 15. Класс CLock с новым методом.

class CLock{ friend class CScopeLock; CRITICAL_SECTION m_CS;public: void Init() { ::InitializeCriticalSection(&m_CS); } void Term() { ::DeleteCriticalSection(&m_CS); } void Lock() { ::EnterCriticalSection(&m_CS); } BOOL TryLock() { return ::TryEnterCriticalSection(&m_CS); } void Unlock() { ::LeaveCriticalSection(&m_CS); } BOOL Check() { return CheckCriticalSection(&m_CS); }};

Использовать метод Check() в release-конфигурациях не стоит, возможно, что в будущем, в какой-нибудь Windows64, структура RTL_CRITICAL_SECTION изменится, и результат такой проверки будет не определен. Так что ему самое место "жить" внутри всяческих ASSERT'ов.

Итак, что мы имеем? Мы имеем проверку на лишний вызов ::LeaveCriticalSection() и ту же трассировку для блокировок. Не так уж много. Особенно если трассировка о блокировке имеет место, а вот нить, забывшая освободить критическую секцию, давно завершилась. Как быть? Вернее, что бы еще придумать, чтобы ошибку проще было выявить? Как минимум, прикрутить сюда __LINE__ и __FILE__, константы, соответствующие текущей строке и имени файла на момент компиляции этого метода.

VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION pcs , int nLine = __LINE__, azFile = __FILE__);

Компилируем, запускаем... Результат удивительный. Хотя правильный. Компилятор честно подставил номер строки и имя файла, соответствующие началу нашей EnterCriticalSectionDbg(). Так что придется попотеть немного больше. __LINE__ и __FILE__ нужно вставить в #define'ы, тогда мы получим действительные номер строки и имя исходного файла. Теперь вопрос, куда же сохранить эти параметры для дальнейшего использования? Причем хочется оставить за собой возможность вызова стандартных функций API наряду с нашими собственными? На помощь приходит C++: просто создадим свою структуру, унаследовав ее от RTL_CRITICAL_SECTION (листинг 16).

Листинг 16. Реализация критических секций с сохранением строки и имени файла.

#if defined(_DEBUG) && !defined(_NO_DEADLOCK_TRACE)#define DEADLOCK_TIMEOUT 30000#define CS_DEBUG 2// Нашаструктуравзамен CRITICAL_SECTIONstruct CRITICAL_SECTION_DBG : public CRITICAL_SECTION{ // Добавочные поля int m_nLine;LPCSTR m_azFile;};typedef struct CRITICAL_SECTION_DBG *LPCRITICAL_SECTION_DBG;// Создаем на лету событие для операций ожидания,// но никогда его не освобождаем. Такудобнейдляотладки.static inline HANDLE _CriticalSectionGetEvent(LPCRITICAL_SECTION pcs){ HANDLE ret = pcs->LockSemaphore; if (!ret) { HANDLE sem = ::CreateEvent(NULL, false, false, NULL); ATLASSERT(sem); if (!(ret = (HANDLE)::InterlockedCompareExchangePointer( &pcs->LockSemaphore, sem, NULL))) ret = sem;else ::CloseHandle(sem); // Кто-то успел раньше } return ret;}// Ждем, пока критическая секция не освободится либо время ожидания// будетпревышеноstatic inline VOID _WaitForCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs , int nLine, LPCSTR azFile){ HANDLE sem = _CriticalSectionGetEvent(pcs); DWORD dwWait; do { dwWait = ::WaitForSingleObject(sem, DEADLOCK_TIMEOUT); if (WAIT_TIMEOUT == dwWait) { ATLTRACE("Critical section timeout (%u msec):" " tid 0x%04X owner tid 0x%04X&bsol;n" "Owner lock from %hs line %u, waiter %hs line %u&bsol;n" , DEADLOCK_TIMEOUT , ::GetCurrentThreadId(), pcs->OwningThread , pcs->m_azFile, pcs->m_nLine, azFile, nLine); } }while(WAIT_TIMEOUT == dwWait); ATLASSERT(WAIT_OBJECT_0 == dwWait);}// Выставляемсобытиевактивноесостояниеstatic inline VOID _UnWaitCriticalSectionDbg(LPCRITICAL_SECTION pcs){ HANDLE sem = _CriticalSectionGetEvent(pcs); BOOL b = ::SetEvent(sem);ATLASSERT(b);}// Инициализируем критическую секцию.inline VOID InitializeCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs){ // Пусть система заполнит свои поля InitializeCriticalSection(pcs); // Заполняем наши поля pcs->m_nLine = 0; pcs->m_azFile = NULL;}// Освобождаем ресурсы, занимаемые критической секциейinline VOID DeleteCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs){ // Проверяем, чтобы не было удалений "захваченных" критических секцийATLASSERT(0 == pcs->m_nLine && NULL == pcs->m_azFile);// Остальное доделает система DeleteCriticalSection(pcs);}// Заполучаем критическую секцию в свое пользованиеinline VOID EnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs , int nLine, LPSTR azFile){ if (::InterlockedIncrement(&pcs->LockCount)){ // LockCount стал больше нуля. // Проверяем идентификатор нитиif (pcs->OwningThread == (HANDLE)::GetCurrentThreadId()){ // Нить та же самая. Критическая секция наша. // Никаких дополнительных действий не производим. // Это не совсем верно, так как возможно, что непарный // вызов ::LeaveCriticalSection() был сделан на n-ном заходе, // и это придется отлавливать вручную, но реализация // стека для __LINE__ и __FILE__ сделает нашу систему // более громоздкой. Если это действительно необходимо, // вы всегда можете сделать это самостоятельно pcs->RecursionCount++; return; } // Критическая секция занята другой нитью. // Придется подождать _WaitForCriticalSectionDbg(pcs, nLine, azFile); } // Либо критическая секция была "свободна", // либо мы дождались. Сохраняем идентификатор текущей нити.pcs->OwningThread = (HANDLE)::GetCurrentThreadId(); pcs->RecursionCount = 1; pcs->m_nLine = nLine; pcs->m_azFile = azFile;}// Заполучаем критическую секцию, если она никем не занятаinline BOOL TryEnterCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs , int nLine, LPSTR azFile){ if (-1L == ::InterlockedCompareExchange(&pcs->LockCount, 0, -1)){ // Это первое обращение к критической секцииpcs->OwningThread = (HANDLE)::GetCurrentThreadId(); pcs->RecursionCount = 1; pcs->m_nLine = nLine; pcs->m_azFile = azFile; } else if (pcs->OwningThread == (HANDLE)::GetCurrentThreadId()){ // Это не первое обращение, но из той же нити::InterlockedIncrement(&pcs->LockCount); pcs->RecursionCount++;} else return FALSE; // Критическая секция занята другой нитьюreturn TRUE;}// Освобождаемкритическуюсекциюinline VOID LeaveCriticalSectionDbg(LPCRITICAL_SECTION_DBG pcs){ // Проверяем, чтобы идентификатор текущей нити совпадал // с идентификатором нити-влядельца. // Если это не так, скорее всего мы имеем дело с ошибкойATLASSERT(pcs->OwningThread == (HANDLE)::GetCurrentThreadId());if (--pcs->RecursionCount) { // Не последний вызов из этой нити. // Уменьшаем значение поля LockCount ::InterlockedDecrement(&pcs->LockCount); } else { // Последний вызов. Нужно "разбудить" какую-либо // из ожидающих ниток, если таковые имеютсяATLASSERT(NULL != pcs->OwningThread); pcs->OwningThread = NULL; pcs->m_nLine = 0; pcs->m_azFile = NULL; if (::InterlockedDecrement(&pcs->LockCount) >= 0) { // Имеется, какминимум, однаожидающаянить _UnWaitCriticalSectionDbg(pcs); } }}// Удостоверяемся, что ::EnterCriticalSection() былавызвана// довызоваэтогометодаinline BOOL CheckCriticalSection(LPCRITICAL_SECTION pcs){ return pcs->LockCount >= 0 && pcs->OwningThread == (HANDLE)::GetCurrentThreadId();}// Переопределяем все функции для работы с критическими секциями.// Определение класса CLock должно быть после этих строк#define InitializeCriticalSection InitializeCriticalSectionDbg#define InitializeCriticalSectionAndSpinCount(pcs, c) &bsol; InitializeCriticalSectionDbg(pcs)#define DeleteCriticalSection DeleteCriticalSectionDbg#define EnterCriticalSection(pcs) EnterCriticalSectionDbg(pcs, __LINE__, __FILE__)#define TryEnterCriticalSection(pcs) &bsol; 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).