~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 _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() в этой нити никогда не вернется. Он будет вечно ждать неизвестно чего.
Читать дальше