pPrevObject; p>
m_lockObject.Lock (); p>
pPrevObject.Attach (m_pObject.Detach ()); p>
m_pObject = pNewobject; p>
m_lockObject.Unlock (); p>
// pPrevObject.Release (); p>
) p>
Тепер потенційно останній дзвінок
IObject2:: Release () буде здійснений після виходу з критичної секції. А
присвоєння нового значення як і раніше синхронізовано з викликом
IObject2:: SomeMethod () з нитки № 1. P>
Способи виявлення помилок
h2>
Спочатку варто звернути увагу на
"офіційний" спосіб виявлення блокувань. Якби окрім
:: 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? Ні? А
даремно. Ви втрачаєте виняткову можливість побачити цю чудову рядок. P>
Ну, а які у нас альтернативи? Так, мабуть, тільки
один. Не використовувати API для роботи з критичними секціями. Замість них
написати свої власні. Нехай навіть не такі обточені напильником, як у
Windows NT. Не страшно. Нам це знадобиться тільки в debug-конфігураціях. У
release'ах ми будемо продовжувати використовувати оригінальний API від Microsoft. Для
цього напишемо кілька функцій, повністю сумісних за типами та кількістю
аргументів з "справжнім" API, і додамо # define, як у MFC, для
перевизначення оператора new в debug-конфігураціях. p>
Лістинг 14. Власна реалізація критичних секцій. P>
# if defined (_DEBUG) & &! defined (_NO_DEADLOCK_TRACE) p>
# define DEADLOCK_TIMEOUT 30000 p>
# define CS_DEBUG 1 p>
// Створюємо на льоту подія
для операцій очікування, p>
// але ніколи його не
звільняємо. Так
зручніше для налагодження p>
static inline HANDLE _CriticalSectionGetEvent (LPCRITICAL_SECTION pcs) p>
( p>
HANDLE ret =
pcs-> LockSemaphore; p>
if (! ret) p>
( p>
HANDLE sem =
:: CreateEvent (NULL, false, false, NULL); p>
ATLASSERT (sem); p>
if (! (ret =
(HANDLE):: InterlockedCompareExchangePointer ( p>
& pcs-> LockSemaphore, sem, NULL ))) p>
ret = sem; p>
else p>
:: CloseHandle (sem);// Хтось встиг
раніше p>
) p>
return ret; p>
) p>
// Чекаємо, поки критична
секція не звільниться небудь час очікування p>
// буде перевищено p>
static inline VOID _WaitForCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
HANDLE sem =
_CriticalSectionGetEvent (Pcs); p>
DWORD dwWait; p>
do p>
( p>
dwWait =
:: WaitForSingleObject (sem, DEADLOCK_TIMEOUT); p>
if (WAIT_TIMEOUT == dwWait) p>
( p>
ATLTRACE ( "Critical
section timeout (% u msec ):" p>
"tid 0x% 04X
owner tid 0x% 04Xn ", DEADLOCK_TIMEOUT, p>
:: GetCurrentThreadId (), pcs-> OwningThread); p>
) p>
) while (WAIT_TIMEOUT ==
dwWait); p>
ATLASSERT (WAIT_OBJECT_0 == dwWait); p>
) p>
// Виставляємо подія в
активний стан p>
static inline VOID _UnWaitCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
HANDLE sem =
_CriticalSectionGetEvent (Pcs); p>
BOOL b =:: SetEvent (sem); p>
ATLASSERT (b); p>
) p>
// Заполучаем критичну
секцію в своє користування p>
inline VOID EnterCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
if
(:: InterlockedIncrement (& pcs-> LockCount)) p>
( p>
// LockCount став більше нуля. p>
// Перевіряємо ідентифікатор нитки p>
if (pcs-> OwningThread ==
(HANDLE):: GetCurrentThreadId ()) p>
( p>
// Нитка та ж сама. Критична секція
наша. p>
pcs-> RecursionCount ++; p>
return; p>
) p>
// Критична секція зайнята інший
ниткою. p>
// Доведеться почекати p>
_WaitForCriticalSectionDbg (pcs); p>
) p>
// Або критична секція була
"вільна", p>
// або ми дочекалися. Зберігаємо
ідентифікатор поточної нитки. p>
pcs-> OwningThread =
(HANDLE):: GetCurrentThreadId (); p>
pcs-> RecursionCount = 1; p>
) p>
// Заполучаем критичну
секцію, якщо вона ніким не зайнята p>
inline BOOL TryEnterCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
if (-1L ==
:: InterlockedCompareExchange (& pcs-> LockCount, 0, -1)) p>
( p>
// Це перше звернення до критичної
секції p>
pcs-> OwningThread =
(HANDLE):: GetCurrentThreadId (); p>
pcs-> RecursionCount = 1; p>
) p>
else if (pcs-> OwningThread
== (HANDLE):: GetCurrentThreadId ()) p>
( p>
// Це не перше звернення, але з тієї ж
нитки p>
:: InterlockedIncrement (& pcs-> LockCount); p>
pcs-> RecursionCount ++; p>
) p>
else p>
return FALSE;// Критична секція
зайнята іншою ниткою p>
return TRUE; p>
) p>
// Звільняємо критичну секцію p>
inline VOID LeaveCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
// Перевіряємо, щоб ідентифікатор поточної
нитки збігався p>
// з ідентифікатором нитки-власника. p>
// Якщо це не так, швидше за все ми маємо
справу з помилкою p>
ATLASSERT (pcs-> OwningThread ==
(HANDLE):: GetCurrentThreadId ()); p>
if (- pcs-> RecursionCount) p>
( p>
// Не останній виклик з цієї нитки. p>
// Зменшуємо значення поля LockCount p>
:: InterlockedDecrement (& pcs-> LockCount); p>
) p>
else p>
( p>
// Останній дзвінок. Потрібно
"розбудити" яку-небудь p>
// з очікують ниток, якщо такі
є p>
ATLASSERT (NULL! = pcs-> OwningThread); p>
pcs-> OwningThread = NULL; p>
if
(:: InterlockedDecrement (& pcs-> LockCount)> = 0) p>
( p>
// Є, як мінімум, один очікує нитка p>
_UnWaitCriticalSectionDbg (Pcs); p>
) p>
) p>
) p>
// засвідчує, що:: EnterCriticalSection () була викликана p>
// до виклику цього методу p>
inline BOOL CheckCriticalSection (LPCRITICAL_SECTION pcs) p>
( p>
return pcs-> LockCount> =
0 p>
& &
pcs-> OwningThread == (HANDLE):: GetCurrentThreadId (); p>
) p>
// перевизначають все
функції для роботи з критичними секціями. p>
// Визначення класу CLock
повинно бути після цих рядків p>
# define EnterCriticalSection EnterCriticalSectionDbg p>
# define TryEnterCriticalSection TryEnterCriticalSectionDbg p>
# define
LeaveCriticalSection LeaveCriticalSectionDbg p>
# endif p>
Ну і заразом додамо ще один метод в наш клас Clock
(лістинг 15). p>
Лістинг 15. Клас CLock з новим методом. P>
class CLock p>
( p>
friend class CScopeLock; p>
CRITICAL_SECTION m_CS; p>
public: p>
void Init () (
:: InitializeCriticalSection (& m_CS);) p>
void Term () (
:: DeleteCriticalSection (& m_CS);) p>
void Lock () (
:: EnterCriticalSection (& m_CS);) p>
BOOL TryLock () (return
:: TryEnterCriticalSection (& m_CS);) p>
void Unlock () (
:: LeaveCriticalSection (& m_CS);) p>
BOOL Check () (return
CheckCriticalSection (& m_CS);) p>
); p>
Використовувати метод Check () в release-конфігурація не
стоїть, можливо, що в майбутньому, в якій-небудь Windows64, структура
RTL_CRITICAL_SECTION зміниться, і результат такої перевірки буде не визначений.
Так що йому саме місце "жити" всередині усіляких ASSERT'ов. P>
Отже, що ми маємо? Ми маємо перевірку на зайвий виклик
:: LeaveCriticalSection () і ту ж трасування для блокувань. Не так вже й багато.
Особливо якщо трасування про блокування має місце, а ось нитка, яка забула
звільнити критичну секцію, давно завершилася. Як бути? Вірніше, що б ще
придумати, щоб помилку простіше було виявити? Як мінімум, прикрутити сюди
__LINE__ І __FILE__, константи, що відповідають поточному рядку й імені файлу на
момент компіляції цього методу. p>
VOID
EnterCriticalSectionDbg (LPCRITICAL_SECTION pcs p>
, int nLine = __LINE__, azFile = __FILE__); p>
Компіліруем, запускаємо ... Результат дивовижний. Хоча
правильний. Компілятор чесно підставив номер рядка та ім'я файлу,
відповідні початку нашої EnterCriticalSectionDbg (). Так що доведеться
попотіти трохи більше. __LINE__ І __FILE__ потрібно вставити в # define'и, тоді
ми отримаємо дійсні номер рядка та ім'я вихідного файлу. Тепер питання,
куди ж зберегти ці параметри для подальшого використання? Причому хочеться
залишити за собою можливість виклику стандартних функцій API поряд з нашими
власними? На допомогу приходить C + +: просто створимо свою структуру,
успадкувавши її від RTL_CRITICAL_SECTION (лістинг 16). p>
Лістинг 16. Реалізація критичних секцій з
збереженням рядку й імені файлу. p>
# if defined (_DEBUG) & &! defined (_NO_DEADLOCK_TRACE) p>
# define DEADLOCK_TIMEOUT 30000 p>
# define CS_DEBUG 2 p>
// Наша структура замість CRITICAL_SECTION p>
struct CRITICAL_SECTION_DBG: public CRITICAL_SECTION p>
( p>
// Додаткові поля p>
int
m_nLine; p>
LPCSTR
m_azFile; p>
); p>
typedef struct CRITICAL_SECTION_DBG * LPCRITICAL_SECTION_DBG; p>
// Створюємо на льоту подія
для операцій очікування, p>
// але ніколи його не
звільняємо. Так
зручніше для налагодження. p>
static inline HANDLE _CriticalSectionGetEvent (LPCRITICAL_SECTION pcs) p>
( p>
HANDLE ret =
pcs-> LockSemaphore; p>
if (! ret) p>
( p>
HANDLE sem =
:: CreateEvent (NULL, false, false, NULL); p>
ATLASSERT (sem); p>
if (! (ret =
(HANDLE):: InterlockedCompareExchangePointer ( p>
& pcs-> LockSemaphore, sem, NULL ))) p>
ret = sem; p>
else p>
:: CloseHandle (sem);// Хтось встиг
раніше p>
) p>
return ret; p>
) p>
// Чекаємо, поки критична
секція не звільниться небудь час очікування p>
// буде перевищено p>
static inline VOID _WaitForCriticalSectionDbg (LPCRITICAL_SECTION_DBG
pcs p>
, int nLine, LPCSTR azFile) p>
( p>
HANDLE sem =
_CriticalSectionGetEvent (Pcs); p>
DWORD dwWait; p>
do p>
( p>
dwWait =
:: WaitForSingleObject (sem, DEADLOCK_TIMEOUT); p>
if (WAIT_TIMEOUT == dwWait) p>
( p>
ATLTRACE ( "Critical
section timeout (% u msec ):" p>
"tid 0x% 04X owner
tid 0x% 04Xn " p>
"Owner lock from
% hs line% u, waiter% hs line% un " p>
, DEADLOCK_TIMEOUT p>
,
:: GetCurrentThreadId (), pcs-> OwningThread p>
, pcs-> m_azFile,
pcs-> m_nLine, azFile, nLine); p>
) p>
) while (WAIT_TIMEOUT ==
dwWait); p>
ATLASSERT (WAIT_OBJECT_0 ==
dwWait); p>
) p>
// Виставляємо подія в активний стан p>
static inline VOID _UnWaitCriticalSectionDbg (LPCRITICAL_SECTION pcs) p>
( p>
HANDLE sem =
_CriticalSectionGetEvent (Pcs); p>
BOOL b =:: SetEvent (sem); p>
ATLASSERT (b); p>
) p>
// Ініціалізіруем критичну
секцію. p>
inline VOID InitializeCriticalSectionDbg (LPCRITICAL_SECTION_DBG pcs) p>
( p>
// Нехай система заповнить свої поля p>
InitializeCriticalSection (pcs); p>
// Заповнюємо наші поля p>
pcs-> m_nLine = 0; p>
pcs-> m_azFile = NULL; p>
) p>
// Звільняємо ресурси, які займає
критичною секцією p>
inline VOID DeleteCriticalSectionDbg (LPCRITICAL_SECTION_DBG pcs) p>
( p>
// Перевіряємо, щоб не було вилучень
"захоплених" критичних секцій p>
ATLASSERT (0 == pcs-> m_nLine & &
NULL == pcs-> m_azFile); p>
// Останнє доробить система p>
DeleteCriticalSection (pcs); p>
) p>
// Заполучаем критичну
секцію в своє користування p>
inline VOID EnterCriticalSectionDbg (LPCRITICAL_SECTION_DBG pcs p>
, int nLine, LPSTR azFile) p>
( p>
if
(:: InterlockedIncrement (& pcs-> LockCount)) p>
( p>
// LockCount став більше нуля. p>
// Перевіряємо ідентифікатор нитки p>
if (pcs-> OwningThread ==
(HANDLE):: GetCurrentThreadId ()) p>
( p>
// Нитка та ж сама. Критична секція
наша. p>
// Ніяких додаткових дій не
виробляємо. p>
// Це не зовсім вірно, тому що
можливо, що непарний p>
// виклик:: LeaveCriticalSection () був
зроблений на n-ному заході, p>
// і це доведеться відловлювати вручну,
але реалізація p>
// стека для __LINE__ і __FILE__
зробить нашу систему p>
// більш громіздкою. Якщо це дійсно
необхідно, p>
// ви завжди можете зробити це
самостійно p>
pcs-> RecursionCount ++; p>
return; p>
) p>
// Критична секція зайнята інший
ниткою. p>
// Доведеться почекати p>
_WaitForCriticalSectionDbg (pcs, nLine,
azFile); p>
) p>
// Або критична секція була
"вільна", p>
// або ми дочекалися. Зберігаємо
ідентифікатор поточної нитки. p>
pcs-> OwningThread =
(HANDLE):: GetCurrentThreadId (); p>
pcs-> RecursionCount = 1; p>
pcs-> m_nLine = nLine; p>
pcs-> m_azFile = azFile; p>
) p>
// Заполучаем критичну
секцію, якщо вона ніким не зайнята p>
inline BOOL TryEnterCriticalSectionDbg (LPCRITICAL_SECTION_DBG pcs p>
, int nLine, LPSTR azFile) p>
( p>
if (-1L ==
:: InterlockedCompareExchange (& pcs-> LockCount, 0, -1)) p>
( p>
// Це перше звернення до критичної
секції p>
pcs-> OwningThread =
(HANDLE):: GetCurrentThreadId (); p>
pcs-> RecursionCount = 1; p>
pcs-> m_nLine = nLine; p>
pcs-> m_azFile = azFile; p>
) p>
else if (pcs-> OwningThread
== (HANDLE):: GetCurrentThreadId ()) p>
( p>
// Це не перше звернення, але з тієї ж
нитки p>
:: InterlockedIncrement (& pcs-> LockCount); p>
pcs-> RecursionCount ++; p>
) p>
else p>
return FALSE;// Критична секція
зайнята іншою ниткою p>
return TRUE; p>
) p>
// Звільняємо критичну секцію p>
inline VOID LeaveCriticalSectionDbg (LPCRITICAL_SECTION_DBG pcs) p>
( p>
// Перевіряємо, щоб ідентифікатор поточної
нитки з?? впадав p>
// з ідентифікатором нитки-влядельца. p>
// Якщо це не так, швидше за все ми маємо
справу з помилкою p>
ATLASSERT (pcs-> OwningThread ==
(HANDLE):: GetCurrentThreadId ()); p>
if (- pcs-> RecursionCount) p>
( p>
// Не останній виклик з цієї нитки. p>
// Зменшуємо значення поля LockCount p>
:: InterlockedDecrement (& pcs-> LockCount); p>
) p>
else p>
( p>
// Останній дзвінок. Потрібно
"розбудити" яку-небудь p>
// з очікують ниток, якщо такі
є p>
ATLASSERT (NULL! = pcs-> OwningThread); p>
pcs-> OwningThread = NULL; p>
pcs-> m_nLine = 0; p>
pcs-> m_azFile = NULL; p>
if
(:: InterlockedDecrement (& pcs-> LockCount)> = 0) p>
( p>
// Є, як мінімум, один очікує нитка p>
_UnWaitCriticalSectionDbg (pcs); p>
) p>
) p>
) p>
// засвідчує, що:: EnterCriticalSection () була викликана p>
// до виклику цього методу p>
inline BOOL CheckCriticalSection (LPCRITICAL_SECTION pcs) p>
( p>
return pcs-> LockCount> =
0 p>
& &
pcs-> OwningThread == (HANDLE):: GetCurrentThreadId (); p>
) p>
// перевизначають все
функції для роботи з критичними секціями. p>
// Визначення класу CLock
повинно бути після цих рядків p>
# define InitializeCriticalSection InitializeCriticalSectionDbg p>
# define InitializeCriticalSectionAndSpinCount (pcs, c) p>
InitializeCriticalSectionDbg (pcs) p>
# define DeleteCriticalSection DeleteCriticalSectionDbg p>
# define EnterCriticalSection (pcs) EnterCriticalSectionDbg (pcs,
__LINE__, __FILE__) P>
# define TryEnterCriticalSection (pcs) p>
TryEnterCriticalSectionDbg (pcs, __LINE__, __FILE__) p>
# define LeaveCriticalSection LeaveCriticalSectionDbg p>
# define CRITICAL_SECTION CRITICAL_SECTION_DBG p>
# define LPCRITICAL_SECTION LPCRITICAL_SECTION_DBG p>
# define PCRITICAL_SECTION PCRITICAL_SECTION_DBG p>
# endif p>
Наводимо наші класи у відповідність (лістинг +17). p>
Лістинг 17. Класи CLock і CScopeLock, варіант для
налагодження. p>
class CLock p>
( p>
friend class CScopeLock; p>
CRITICAL_SECTION m_CS; p>
public: p>
void Init () (
:: InitializeCriticalSection (& m_CS);) p>
void Term () (
:: DeleteCriticalSection (& m_CS);) p>
# if defined (CS_DEBUG) p>
BOOL Check () (return
CheckCriticalSection (& m_CS);) p>
# endif p>
# if CS_DEBUG> 1 p>
void Lock (int nLine, LPSTR
azFile) (EnterCriticalSectionDbg (& m_CS, nLine, azFile);) p>
BOOL TryLock (int nLine, LPSTR
azFile) (return TryEnterCriticalSectionDbg (& m_CS, nLine, azFile);) p>
# else p>
void Lock () (
:: EnterCriticalSection (& m_CS);) p>
BOOL TryLock () (return
:: TryEnterCriticalSection (& m_CS);) p>
# endif p>
void Unlock () (
:: LeaveCriticalSection (& m_CS);) p>
); p>
class CScopeLock p>
( p>
LPCRITICAL_SECTION m_pCS; p>
public: p>
# if CS_DEBUG> 1 p>
CScopeLock (LPCRITICAL_SECTION
pCS, int nLine, LPSTR azFile): m_pCS (pCS) (Lock (nLine, azFile);) p>
CScopeLock (CLock & lock,
int nLine, LPSTR azFile): m_pCS (& lock.m_CS) (Lock (nLine, azFile);) p>
void Lock (int nLine, LPSTR
azFile) (EnterCriticalSectionDbg (m_pCS, nLine, azFile);) p>
# else p>
CScopeLock (LPCRITICAL_SECTION
pCS): m_pCS (pCS) (Lock ();) p>
CScopeLock (CLock & lock):
m_pCS (& lock.m_CS) (Lock ();) p>
void Lock () (:: EnterCriticalSection (m_pCS);
) p>
# endif p>
~ CScopeLock () (Unlock ();) p>
void Unlock () (
:: LeaveCriticalSection (m_pCS);) p>
); p>
# if CS_DEBUG> 1 p>
# define Lock () Lock (__LINE__, __FILE__) p>
# define TryLock () TryLock (__LINE__, __FILE__) p>
# define lock (cs) lock (cs, __LINE__, __FILE__) p>
# endif p>
На жаль, довелося навіть перевизначити CScopeLock
lock (cs), причому жорстко прив'язатися до імені змінної. Не варто говорити про
те, що напевно вийшов конфлікт імен - все-таки Lock досить популярне
назва для методу. Такий код не буде збиратися, наприклад, з популярною
бібліотекою ATL. Тут є два способи. Перейменувати методи Lock () і TryLock ()
у що-небудь більш унікальне, або перейменувати Lock () в ATL: p>
// StdAfx.h p>
//. .. p>
# define Lock ATLLock p>
# include p>
//. .. p>
Сме