}
// Либо критическая секция была "свободна",
// либо мы дождались. Сохраняем идентификатор текущей нити.
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. Класс 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. Реализация критических секций с сохранением строки и имени файла
#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;
}
// Освобождаем критическую секцию