Реферат по предмету "Программирование"


Критические секции

Критические секции

Павел Блудов
Введение


Критические секции -- это объекты, используемые для
блокировки доступа всех нитей (threads) приложения, кроме одной, к некоторым
важным данным в один момент времени. Например, имеется переменная m_pObject и
несколько нитей, вызывающих методы объекта, на который ссылается m_pObject,
причем эта переменная может изменять свое значение время от времени. Иногда там
даже оказывается нуль. Предположим, имеется вот такой код:




// Нить №1


void Proc1()


{


    if (m_pObject)


       
m_pObject->SomeMethod();


}



// Нить №2


void Proc2(IObject *pNewObject)


{


    if (m_pObject)


        delete m_pObject;


    m_pObject = pNewobject;


}






Тут мы имеем потенциальную опасность вызова
m_pObject->SomeMethod() после того, как объект был уничтожен при помощи
delete m_pObject. Дело в том, что в системах с вытесняющей многозадачностью
выполнение любой нити процесса может прерваться в самый неподходящий для нее
момент времени, и начнет выполняться совершенно другая нить. В данном примере
неподходящим моментом будет тот, в котором нить №1 уже проверила m_pObject, но
еще не успела вызвать SomeMethod(). Выполнение нити №1 прервалось, и начала
исполняться нить №2. Причем нить №2 успела вызвать деструктор объекта. Что же
произойдет, когда нить №1 получит немного процессорного времени и вызовет-таки
SomeMethod() у уже несуществующего объекта? Наверняка что-то ужасное.

Именно тут приходят на помощь критические секции.
Перепишем наш пример.




// Нить №1


void Proc1()


{


   
::EnterCriticalSection(&m_lockObject);


    if (m_pObject)


       
m_pObject->SomeMethod();


   
::LeaveCriticalSection(&m_lockObject);


}



// Нить №2


void Proc2(IObject *pNewObject)


{


   
::EnterCriticalSection(&m_lockObject);


    if (m_pObject)


        delete m_pObject;


    m_pObject = pNewobject;


    ::LeaveCriticalSection(&m_lockObject);


}






Код, помещенный между ::EnterCriticalSection() и
::LeaveCriticalSection() с одной и той же критической секцией в качестве
параметра, никогда не будет выполняться параллельно. Это означает, что если
нить №1 успела "захватить" критическую секцию m_lockObject, то при
попытке нити №2 заполучить эту же критическую секцию в свое единоличное
пользование, ее выполнение будет приостановлено до тех пор, пока нить №1 не
"отпустит" m_lockObject при помощи вызова ::LeaveCriticalSection(). И
наоборот, если нить №2 успела раньше нити №1, то та "подождет",
прежде чем начнет работу с m_pObject.

Работа с критическими секциями

Что же происходит внутри критических секций и как они
устроены? Прежде всего, следует отметить, что критические секции – это не
объекты ядра операционной системы. Практически вся работа с критическими
секциями происходит в создавшем их процессе. Из этого следует, что критические
секции могут быть использованы только для синхронизации в пределах одного
процесса. Теперь рассмотрим критические секции поближе.

Структура RTL_CRITICAL_SECTION





typedef struct _RTL_CRITICAL_SECTION {


    PRTL_CRITICAL_SECTION_DEBUG
DebugInfo; // Используется операционной системой


    LONG LockCount;        // Счетчик использования этой
критической секции


    LONG RecursionCount;    // Счетчик повторного захвата из
нити-владельца


    HANDLE OwningThread;    // Уникальный ID нити-владельца


    HANDLE LockSemaphore;    // Объект ядра используемый для ожидания


    ULONG_PTR SpinCount;    // Количество холостых циклов перед
вызовом ядра


} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;






Поле LockCount увеличивается на единицу при каждом
вызове ::EnterCriticalSection() и уменьшается при каждом вызове
::LeaveCriticalSection(). Это первая (а часто и единственная проверка) на пути
к "захвату" критической секции. Если после увеличения в этом поле
находится ноль, это означает, что до этого момента непарных вызовов ::EnterCriticalSection()
из других ниток не было. В этом случае можно забрать данные, охраняемые этой
критической секцией в монопольное пользование. Таким образом, если критическая
секция интенсивно используется не более чем одной нитью,
::EnterCriticalSection() практически вырождается в ++LockCount, а
::LeaveCriticalSection() в --LockCount. Это очень важно. Это означает, что
использование многих тысяч критических секций в одном процессе не повлечет
значительного расхода ни системных ресурсов, ни процессорного времени.




СОВЕТ


Не стоит экономить на критических
секциях. Много cэкономить все равно не получится.






В поле RecursionCount хранится количество повторных
вызовов ::EnterCriticalSection() из одной и той же нити. Действительно, если
вызвать ::EnterCriticalSection() из одной и той же нити несколько раз, все
вызовы будут успешны. Т.е. вот такой код не остановится навечно во втором
вызове ::EnterCriticalSection(), а отработает до конца.




// Нить №1


void Proc1()


{


   
::EnterCriticalSection(&m_lock);


    //. ..


    Proc2()


    //. ..


   
::LeaveCriticalSection(&m_lock);


}



// Все еще нить №1


void Proc2()


{


   
::EnterCriticalSection(&m_lock);


    //. ..


    ::LeaveCriticalSection(&m_lock);


}






Действительно, критические секции предназначены для
защиты данных от доступа из нескольких ниток. Многократное использование одной
и той же критической секции из одной нити не приведет к ошибке. Это вполне
нормальное явление. Следите, чтобы количество вызовов ::EnterCriticalSection()
и ::LeaveCriticalSection() совпадало, и все будет хорошо.

Поле OwningThread содержит 0 для никем не занятых
критических секций или уникальный идентификатор нити-владельца. Это поле
проверяется, если при вызове ::EnterCriticalSection() поле LockCount после
увеличения на единицу оказалось больше нуля. Если OwningThread совпадает с
уникальным идентификатором текущей нити, то RecursionCount просто увеличивается
на единицу и ::EnterCriticalSection() возвращается немедленно. Иначе
::EnterCriticalSection() будет дожидаться, пока нить, владеющая критической
секцией, не вызовет ::LeaveCriticalSection() необходимое количество раз.

Поле LockSemaphore используется, если нужно подождать,
пока критическая секция освободится. Если LockCount больше нуля, и OwningThread
не совпадает с уникальным идентификатором текущей нити, то ждущая нить создает
объект ядра (событие) и вызывает ::WaitForSingleObject(LockSemaphore).
Нить-владелец, после уменьшения RecursionCount, проверяет его, и если значение
этого поля равно нулю, а LockCount больше нуля, то это значит, что есть как
минимум одна нить, ожидающая, пока LockSemaphore не окажется в состоянии
"случилось!". Для этого нить-владелец вызывает ::SetEvent(), и
какая-то одна (только одна) из ожидающих ниток пробуждается и получает доступ к
критическим данным.

WindowsNT/2k генерирует исключение, если попытка
создать событие не увенчалась успехом. Это верно как для функций
::Enter/LeaveCriticalSection(), так и для
::InitializeCriticalSectionAndSpinCount() с установленным старшим битом
параметра SpinCount. Но только не в WindowsXP. Разработчики ядра этой
операционной системы поступили по-другому. Вместо генерации исключения, функции
::Enter/LeaveCriticalSection(), если не могут создать собственное событие,
начинают использовать заранее созданный глобальный объект. Один на всех. Таким
образом, в случае катастрофической нехватки системных ресурсов, программа под
управлением WindowsXP ковыляет какое-то время дальше. Действительно, писать
программы, способные продолжать работать после того, как
::EnterCriticalSection() сгенерировала исключение, чрезвычайно сложно. Как
правило, если программистом и предусмотрен такой поворот событий, то дальше
вывода сообщения об ошибке и аварийного завершения программы дело не идет. Как
следствие, WindowsXP игнорирует старший бит поля LockCount.

И, наконец, поле SpinCount. Это поле используется
только многопроцессорными системами. В однопроцессорных системах, если
критическая секция занята другой нитью, можно только переключить управление на
нее и подождать наступления события. В многопроцессорных системах есть
альтернатива: прогнать некоторое количество раз холостой цикл, проверяя каждый
раз, не освободилась ли наша критическая секция. Если за SpinCount раз это не
получилось, переходим к ожиданию. Это гораздо эффективнее, чем переключение на
планировщик ядра и обратно. Кроме того, в WindowsNT/2k старший бит этого поля
служит для индикации того, что объект ядра, хендл которого находится в поле
LockSemaphore, должен быть создан заранее. Если системных ресурсов для этого
недостаточно, система сгенерирует исключение, и программа может
"урезать" свою функциональность. Или совсем завершить работу.




ПРИМЕЧАНИЕ


Все это верно для Windows NT/2k/XP. В
Windows 9x/Me используется только поле LockCount. Там находится указатель на
объект ядра, возможно, просто взаимоисключение (mutex). Все остальные поля
равны нулю.






API для работы с критическими секциями

BOOL
InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

BOOL
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount);

Заполняют поля структуры, адресуемой
lpCriticalSection. После вызова любой из этих функций критическая секция готова
к работе.

Листинг 1. Псевдокод
RtlInitializeCriticalSection из ntdll.dll




VOID RtlInitializeCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{


 
RtlInitializeCriticalSectionAndSpinCount(pcs, 0)


}


VOID RtlInitializeCriticalSectionAndSpinCount(


  LPRTL_CRITICAL_SECTION pcs,
DWORD dwSpinCount)


{


  pcs->DebugInfo = NULL;


  pcs->LockCount = -1;


  pcs->RecursionCount = 0;


  pcs->OwningThread = 0;


  pcs->LockSemaphore = NULL;


  pcs->SpinCount =
dwSpinCount;


  if (0x80000000 &
dwSpinCount)


    _CriticalSectionGetEvent(pcs);


}






DWORD
SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD
dwSpinCount);

Устанавливает значение поля SpinCount и возвращает его
предыдущее значение. Напоминаю, что старший бит отвечает за
"привязку" события, используемого для ожидания доступа к данной
критической секции.

Листинг 2. Псевдокод
RtlSetCriticalSectionSpinCount из ntdll.dll




DWORD
RtlSetCriticalSectionSpinCount(


  LPRTL_CRITICAL_SECTION pcs, DWORD
dwSpinCount)


{


  DWORD dwRet = pcs->SpinCount;


  pcs->SpinCount = dwSpinCount;


  return
dwRet;


}






VOID
DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Освобождает ресурсы, занимаемые критической секцией.

Листинг
3. Псевдокод RtlDeleteCriticalSection из ntdll.dll




VOID RtlDeleteCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{


  pcs->DebugInfo = NULL;


  pcs->LockCount = -1;


  pcs->RecursionCount = 0;


  pcs->OwningThread = 0;


  if (pcs->LockSemaphore)


  {


   
::CloseHandle(pcs->LockSemaphore);


    pcs->LockSemaphore =
NULL;


  }


}






VOID
EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

BOOL
TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Осуществляют "захват" критической секции.
Если критическая секция занята другой нитью, то ::EnterCriticalSection() будет
ждать, пока та освободится, а ::TryEnterCriticalSection() вернет FALSE.
Отсутствует в Windows 9x/ME.

Листинг
4. Псевдокод RtlEnterCriticalSection из ntdll.dll




VOID RtlEnterCriticalSection(LPRTL_CRITICAL_SECTION pcs)


{


  if
(::InterlockedIncrement(&pcs->LockCount))


  {


    if (pcs->OwningThread ==
(HANDLE)::GetCurrentThreadId())


    {


      pcs->RecursionCount++;


      return;


    }



   
RtlpWaitForCriticalSection(pcs);


  }



  pcs->OwningThread =
(HANDLE)::GetCurrentThreadId();


  pcs->RecursionCount = 1;


}



BOOL RtlTryEnterCriticalSection(LPRTL_CRITICAL_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;


}






VOID
LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

Освобождает критическую секцию,

Листинг 5. Псевдокод
RtlLeaveCriticalSection из ntdll.dll




VOID
RtlLeaveCriticalSectionDbg(LPRTL_CRITICAL_SECTION pcs)


{


  if (--pcs->RecursionCount)


   
::InterlockedDecrement(&pcs->LockCount);


  else if
(::InterlockedDecrement(&pcs->LockCount) >= 0)


    RtlpUnWaitCriticalSection(pcs);


}






Классы-обертки для критических секций


Листинг
6. Код классов CLock, CAutoLock и CScopeLock.




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); }


};



class CAutoLock : public CLock


{


public:


  CAutoLock() { Init(); }


  ~CAutoLock() { Term(); }


};



class CScopeLock


{


  LPCRITICAL_SECTION m_pCS;


public:


  CScopeLock(LPCRITICAL_SECTION pCS) :
m_pCS(pCS) { Lock(); }


  CScopeLock(CLock& lock) :
m_pCS(&lock.m_CS) { Lock(); }


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

Это только цветочки. С ягодками вы очень быстро
столкнетесь, если попытаетесь написать что-нибудь действительно сложное.
Например, 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, и никаких проблем не возникло. Сильно
надуманный пример? Ничуть. Именно переключение ниток лежит в основе
подразделений (apartments) COM. А из этого следует одно очень, очень неприятное
правило.




СОВЕТ


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






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

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




// Нить №1


void Proc1()


{


  m_lockObject.Lock();


  CComPtr
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():

Листинг 13




// Нить №2


void Proc2(IObject *pNewObject)


{


  CComPtr
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 для работы с критическими секциями. Вместо них
написать свои собственные. Пусть даже не такие обточенные напильником, как в
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%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);


  }



  // Либо критическая секция была
"свободна",


  // либо мы дождались. Сохраняем
идентификатор текущей нити.


  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_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;


}



// Освобождаем критическую секцию


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)


    
InitializeCriticalSectionDbg(pcs)


#define DeleteCriticalSection DeleteCriticalSectionDbg


#define EnterCriticalSection(pcs) EnterCriticalSectionDbg(pcs,
__LINE__, __FILE__)


#define TryEnterCriticalSection(pcs)


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

Листинг 17. Классы CLock и CScopeLock, вариант для
отладки.




class CLock


{


  friend class CScopeLock;


  CRITICAL_SECTION m_CS;


public:


  void Init() {
::InitializeCriticalSection(&m_CS); }


  void Term() {
::DeleteCriticalSection(&m_CS); }



#if defined(CS_DEBUG)


  BOOL Check() { return
CheckCriticalSection(&m_CS); }


#endif


#if CS_DEBUG > 1


  void Lock(int nLine, LPSTR
azFile) { EnterCriticalSectionDbg(&m_CS, nLine, azFile); }


  BOOL TryLock(int nLine, LPSTR
azFile) { return TryEnterCriticalSectionDbg(&m_CS, nLine, azFile); }


#else


  void Lock() {
::EnterCriticalSection(&m_CS); }


  BOOL TryLock() { return
::TryEnterCriticalSection(&m_CS); }


#endif


  void Unlock() {
::LeaveCriticalSection(&m_CS); }


};


class CScopeLock


{


  LPCRITICAL_SECTION m_pCS;


public:


#if CS_DEBUG > 1


  CScopeLock(LPCRITICAL_SECTION
pCS, int nLine, LPSTR azFile) : m_pCS(pCS) { Lock(nLine, azFile); }


  CScopeLock(CLock& lock,
int nLine, LPSTR azFile) : m_pCS(&lock.m_CS) { Lock(nLine, azFile); }


  void Lock(int nLine, LPSTR
azFile) { EnterCriticalSectionDbg(m_pCS, nLine, azFile); }


#else


  CScopeLock(LPCRITICAL_SECTION
pCS) : m_pCS(pCS) { Lock(); }


  CScopeLock(CLock& lock) :
m_pCS(&lock.m_CS) { Lock(); }


  void Lock() { ::EnterCriticalSection(m_pCS);
}


#endif


  ~CScopeLock() { Unlock(); }


  void Unlock() {
::LeaveCriticalSection(m_pCS); }


};



#if CS_DEBUG > 1


#define Lock() Lock(__LINE__, __FILE__)


#define TryLock() TryLock(__LINE__, __FILE__)


#define lock(cs) lock(cs, __LINE__, __FILE__)


#endif






К сожалению, пришлось даже переопределить CScopeLock
lock(cs), причем жестко привязаться к имени переменной. Не стоит говорить о
том, что наверняка получился конфликт имен - все-таки Lock довольно популярное
название для метода. Такой код не будет собираться, например, с популярнейшей
библиотекой ATL. Тут есть два способа. Переименовать методы Lock() и TryLock()
во что-нибудь более уникальное, либо переименовать Lock() в ATL:




// StdAfx.h


//. ..


#define Lock ATLLock


#include


//. ..






Сменим тему


А что это мы все про Win32 API да про C++? Давайте
посмотрим, как обстоят дела с критическими секциями в более современных языках
программирования.

C#


Тут стараниями Майкрософт имеется полный набор старого
доброго API под новыми именами.

Критические секции представлены классом
System.Threading.Monitor, вместо ::EnterCriticalSection() есть
Monitor.Enter(object), а вместо ::LeaveCriticalSection() Monitor.Exit(object),
где object – это любой объект C#. Т.е. каждый объект где-то в потрохах CLR
(Common Language Runtime) имеет свою собственную критическую секцию либо
заводит ее по необходимости. Типичное использование этой секции выглядит так:




Monitor.Enter(this);


m_dwSmth
= dwSmth;


Monitor.Exit(this);






Если нужно организовать отдельную критическую секцию
для какой-либо переменной, самым логичным способом будет поместить ее в
отдельный объект и использовать этот объект как аргумент при вызове
Monitor.Enter/Exit(). Кроме того, в C# существует ключевое слово lock, это
полный аналог нашего класса CScopeLock.




lock
(this)


{


  m_dwSmth = dwSmth;


}






А вот Monitor.TryEnter() в C# (о, чудо!) принимает в
качестве параметра максимальный период ожидания.

Замечу, что CLR – это не только C#, все это применимо
и к другим языкам, использующим CLR.

Java


В этом языке используется подобный механизм, только
место ключевого слова lock есть ключевое слово synchronized, а все остальное –
точно так же.




synchronized
(this)


{


  m_dwSmth = dwSmth;


}






MC++ (управляемый C++)


Тут тоже появился атрибут [synchronized] ведущий себя
точно так же, как и одноименное ключевое слово из Java. Странно, что
архитекторы из Майкрософт решили позаимствовать синтаксис из продукта от Sun
Microsystems вместо своего собственного.




[synchronized]
DWORD m_dwSmth;


//...


m_dwSmth
= dwSmth; // неявный вызов Lock(this)






Delphi


Практически все, что верно для C++, верно и для
Delphi. Критические секции представлены объектом TCriticalSection. Собственно,
это такая же обертка как и наш класс CLock.

Кроме того, в Delphi присутствует специальный объект
TMultiReadExclusiveWriteSynchronizer с названием, говорящим само за себя.

Подведем итоги


Итак, что нужно знать о критических секциях:

Критические секции работают быстро и не требуют
большого количества системных ресурсов.

Для синхронизации доступа к нескольким (независимым)
переменным лучше использовать несколько критических секций, а не одну для всех.


Код, ограниченный критическими секциями, лучше всего
свести к минимуму.

Находясь в критической секции, не стоит вызывать
методы "чужих" объектов.
Список литературы

Для подготовки данной работы были использованы
материалы с сайта http://www.rsdn.ru/


Не сдавайте скачаную работу преподавателю!
Данный реферат Вы можете использовать для подготовки курсовых проектов.

Поделись с друзьями, за репост + 100 мильонов к студенческой карме :

Пишем реферат самостоятельно:
! Как писать рефераты
Практические рекомендации по написанию студенческих рефератов.
! План реферата Краткий список разделов, отражающий структура и порядок работы над будующим рефератом.
! Введение реферата Вводная часть работы, в которой отражается цель и обозначается список задач.
! Заключение реферата В заключении подводятся итоги, описывается была ли достигнута поставленная цель, каковы результаты.
! Оформление рефератов Методические рекомендации по грамотному оформлению работы по ГОСТ.

Читайте также:
Виды рефератов Какими бывают рефераты по своему назначению и структуре.