Рейтинговые книги
Читем онлайн Программирование на Visual C++. Архив рассылки - Алекс Jenter

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 138 139 140 141 142 143 144 145 146 ... 156

} 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. Самоблокировка средствами одного объекта

// Нить #1

void IObject1::Proc1() {

 // Входим в критическую секцию объекта #1

 m_lockObject.Lock();

 // Вызываем метод объекта #2, происходит переключение на нить объекта #2

 m_pObject2->SomeMethod();

 // Сюда мы попадем только по возвращении из

 m_pObject2->SomeMethod();

 m_lockObject.Unlock();

}

// Нить #2

void IObject2::SomeMethod() {

 // Вызываем метод объекта #1 из нити объекта #2

 m_pObject1->Proc2();

}

// Нить #2

void IObject1::Proc2() {

 // Пытаемся войти в критическую секцию объекта #1

 m_lockObject.Lock();

 // Сюда мы не попадем никогда

 m_lockObject.Unlock();

}

Если бы в примере не было переключения нитей, все вызовы произошли бы в нити объекта #1, и никаких проблем не возникло. Сильно надуманный пример? Ничуть. Именно переключение ниток лежит в основе подразделений COM (apartments). А из этого следует одно очень, очень неприятное правило.

СОВЕТ

Избегайте вызовов каких бы то ни было объектов при захваченных критических секциях.

Помните пример из начала статьи? Так вот, он абсолютно неприемлем в подобных случаях. Его придется переделать на что-то вроде

Листинг 12. Простой пример, не подверженный самоблокировке

// Нить #1

void Proc1() {

 m_lockObject.Lock();

 CComPtr<IObject> pObject(m_pObject); // вызов pObject->AddRef();

 m_lockObject.Unlock();

 if (pObject) pObject->SomeMethod();

}

// Нить #2

void 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().

// Нить #2 void 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, которое всегда равно нулю и, судя по всему, не используется. Зато используется ключ в реестре "HKLMSYSTEMCurrentControlSetControlSession ManagerCriticalSectionTimeout", который по умолчанию равен 30 суткам, и по истечению этого времени в системный лог попадает строка "RTL: Enter Critical Section Timeout (2 minutes)nRTL: Pid.Tid XXXX.YYYY, owner tid ZZZZnRTL: Re-Waitingn". К тому же это верно только для систем WindowsNT/2k/XP и только с CheckedBuild. У вас установлен CheckedBuild? Нет? А зря. Вы теряете исключительную возможность увидеть эту замечательную строку.

Ну, а какие у нас альтернативы? Да, пожалуй, только одна. Не использовать API для работы с критическими секциями. Вместо них написать свои собственные. Пусть даже не такие обточенные напильником, как в WindowsNT. Не страшно. Нам это понадобится только в 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%04Xn", 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);

1 ... 138 139 140 141 142 143 144 145 146 ... 156
На этой странице вы можете бесплатно читать книгу Программирование на Visual C++. Архив рассылки - Алекс Jenter бесплатно.
Похожие на Программирование на Visual C++. Архив рассылки - Алекс Jenter книги

Оставить комментарий