Чтение онлайн

ЖАНРЫ

Программирование на Visual C++. Архив рассылки

Jenter Алекс

Шрифт:

Контроль вызова API функций в среде систем Windows '95, Windows '98 и Windows NT

Это все на сегодня. Пока! 

Алекс Jenter jenter@rsdn.ru Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN. 

Программирование на Visual C++

Выпуск №66 от 3 марта 2002 г.

Здравствуйте, дорогие подписчики!

Помнится, когда-то я уже публиковал статью, посвященную вопросу многозадачности и синхронизации потоков (нитей). Сегодня я предлагаю вам вернуться к этой теме, но уже на более подробном уровне. Вместе с Павлом Блудовым мы подробно рассмотрим один из объектов синхронизации – критические

секции, и причем не просто их применение, но и их внутреннее устройство.

СТАТЬЯ 

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

Автор: Paul Bludov

Демонстрационный проект CSTest (7.8kb)

Файл csdbg.h (1.8kb)

Файл csdbg2.h (2.5kb)

Классы-обертки для критических секций cswrap.h (0.5kb)

Введение

Критические секции – это объекты, используемые для блокироки доступа к некоторорым важным данным всем нитям (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. Это очень важно. Это означает, что использование многих тысяч критических секций в одном процессе не повлечет значительного расхода ни системных ресурсов, ни процессорного времени.

СОВЕТ

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

В поле 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 необходимое количество раз.

Поделиться с друзьями: