Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
376
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

для применения в многопоточной среде, создав для собственных типов данных оболочку из С++-класса Используя такие классы, я попутно представлю объ ект, ведущий себя прямо противоположно семафору.

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

Наконец, я продемонстрирую свою функцию WaitForMultipleExpressions. Работая по аналогии с WaitForMultipleObjects, заставляющей ждать освобождения одного или всех объектов, она позволяет указывать более сложные условия пробуждения потока.

Реализация критической секции: объект-оптекс

Критические секции всегда интересовали меня. В конце концов, ссли это всего лишь объекты пользовательского режима, то почему бы мне не реализовать их самому? Разве нельзя заставить их работать бсз поддержки операционной системы? Кроме того, написав собственную критическую секцию, я мог бы расширить ее функциональ ность и в чем-то даже усовершенствовать. По крайней мере я сделал бы так, чтобы она отслеживала, какой поток захватывает защищаемый ею ресурс. Такая реализация критической секции помогла бы мне устранять проблемы с взаимной блокировкой потоков с помощью отладчика я узнавал бы, какой из них не освободил тот или иной ресурс.

Так что давайте без лишних разговоров перейдем к тому, как реализуются крити ческие секции. Я все время утверждаю, что они являются объектами пользовательс кого режима Нз самом дслс это не совсем так. Любой поток, который пытается вой ти в критическую секцию, уже захваченную другим потоком, переводится в состоя ние ожидания. А для этого он должен перейти из пользовательского режима в режим ядра. Поток пользовательского режима может остановиться, просто войдя в цикл ожидания, но это вряд ли можно назвать эффективной реализацией ждущего режи ма, и поэтому Вы должны всячески избегать ее.

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

Мой вариант критической секции содержится в файлах Optex.h и Optex.cpp (см, листинг на рис. 10-1). Я назвал cc оптимизированным мъютексом оптексом и реализовал в виде С++-класса. Разобравшись в этом коде, Вы поймете, почему крити ческие секции работают быстрее объектов ядра «мьютекс».

Поскольку я создавал собственную критическую секцию, у меня была возможность расширить ее функциональность. Например, мой класс COpcex позволяет синхрони зировать потоки из разных процессов. Это фантастически полезная особенность моей реализации: Вы получаете высокоэффективный механизм взаимодействия между по токами из разных процессов

Чтобы использовать мой оптекс, Вы просто объявляете объект класса COptex. Для этого объекта предусмотрено три конструктора;

COptex::(DWORD dwSpinCount = 4000);

COptex::(PCSTR pszNane, DWORD dwSpinCount = 4000);

COptex::(PCWSTR pszName, DWORD dwSpinCount = 4000);

Первый создает объект COptex, применимый для синхронизации потоков лишь одного процесса. Оптекс этого типа работает быстрее, чем межпроцессный. Осталь ные два конструктора создают оптекс, которым могут пользоваться потоки из разных процессов. В параметре pszName Вы должны передавать ANSIили Unicode-строку, уникально идентифицирующую каждый разделяемый оптекс. Чтобы процессы разде ляли один оптекс, они должны создать по экземпляру объекта COptex с одинаковым именем.

Поток входит в объект COptex и покидает его, вызывая методы Enter и Leave:

void COptex::Enter(); void COptex::Leave();

Я даже включил методы, эквивалентные функциям TryEnterCriticalSection и SetCriti calSectionSpinCount критических секций:

BOOL COptex::TryEnter();

void COptex::SetSpinCount(DWORD dwSpinCount);

Тип оптекса (одноили межпроцессный) позволяет выяснить последний метод класса COptex, показанный ниже. (Необходимость в его вызове возникает очень ред ко, но внутренние функции класса время от времени к нему обращаются.)

BOOL COptex::IsSingleProcessOptex() const;

Вот и все (открытые) функции, о которых Вам нужно знать, чтобы пользоваться оптексом. Теперь я объясню, как работает оптекс. Он — как, в сущности, и критичес кая секция — содержит несколько псрсменных-членов. Значения этих переменных отражают состояние оптекса. Просмотрев файл Optex.h, Вы увидите, что в основном они являются элементами структуры SHAREDINFO, а остальные — членами самого класса. Назначение каждой переменной описывается в следующей таблице.

Переменная

Описание

 

 

m_lLockCount

Сообщает, сколько раз потоки пытались занять оптекс Ее значение равно 0, если оптекс не занят

 

ни одним потоком.

 

 

т dwThreadId

Сообщает уникальный идентификатор потока — владельца оптекса Ее значение равно 0, если

 

оптекс не занят ни одним потоком

 

 

m_lRecurseCount

Указывает, сколько раз отеке был занят потокомвладельцем. Ее зна чение равно 0, если оптекс

 

не занят ни одним потоком.

 

 

m_hevt

Содержит описатель объекта ядра «событие", используемого, только если поток пытается войти в

 

оптекс в то время, как им владеет другой поток. Описатели объектов ядра специфичны для

 

конкретных процес сов, и имении поэтому данная переменная не включена в структуру

 

SHAREDINFO.

 

 

m_dwSpinCount

Определяет, сколько попыток входа в оптекс должен предпринять по ток до перехода в состояние

 

ожидания на объекте ядра «событие». На однопроцессорной машине значение этой переменной

 

всегда равно 0.

 

 

m_hfm

Содержит описатель объекта ядра «проекция файла», используемого при разделении оптекса

 

несколькими процессами Описатели объек тов ядра специфичны для конкретных процессов, и

 

именно поэтому данная переменная не включена в структуру SHAREDINFO. В однопро цессном

 

оптексе значение этой переменной всегда равно NULL

 

 

m_psi

Содержит указатель на элементы данных оптекса, которые могут ис пользоваться несколькими

 

процессами. Адреса памяти специфичны для конкретных процессов, и именно поэтому данная

 

переменная не включена в структуру SHAREDINFO. В однопроцессном оптексе эта пе ременная

 

указывает ни блок памяти, выделенный из кучи, а в межпро цессном — на файл,

 

спроецированный в память.

 

 

Комментариев в исходном коде вполне достаточно, и у Вас не должно возникнуть трудностей в понимании того, как работает оптекс. Важно лишь отметить, что высо кое быстродействие оптекса достигается за счет интенсивного использования lnterlo ced- функций. Благодаря им код выполняется в пользовательском режиме и перехо дит в режим ядра только в том случае, когда это действительно необходимо.

Программа-пример Optex

Эта программа, «10 Optex.exe» (см.листинг на рис, 10-1), предназначенадля провер ки того, что класс COptex работает корректно. Файлы исходного кода и ресурсов этой программы находятся в каталоге 10-Optex на компакт-диске, прилагаемом к книге. Я всегда запускаю такие приложения под управлением отладчика, чтобы наблюдать за всеми функциями и переменными — членами классов,

При запуске программа сначала определяет, является ли она первым экземпляром. Для этого я создаю именованный объект ядра «событие». Реально я им не пользуюсь, а просто смотрю, вернет ли GetLastError значение ERROR_ALREADY_EXISTS. Если да, значит, это второй экземпляр программы. Зачем мнс два экземпляра этой програм мы, я объясню позже.

Если же это первый экземпляр, я создаю однопроцессный объект COptex и вызы ваю свою функцию FirstFunc. Она выполняет серию операций с объектом-оптексом и создает второй поток, который манипулирует тем же оптексом. На этом этапе с оптексом работают два потока из одного процесса. Что именно они делают, Вы узна ете, просмотрев исходный код. Я пытался охватить все мыслимые сценарии, чтобы дать шанс на выполнение каждому блоку кода в классе COptex

После тестирования однопроцессного оптекса я начинаю проверку межпроцесс ного оптекса. В функции _tWinMain по завершении первого вызова FirstFunc я создаю

другой объект-оптекс COptex. Но на этот раз я присваиваю ему имя — CrossOptexTest. Простое присвоение оптексу имени в момент создания превращает этот объект в межпроцессный. Далее я снова вызываю FirstFunc, передавая сй адрес межпроцессно го оптекса При этом FirstFunc выполняет в основном тот же код, что и раньше. Но теперь она порождает не второй поток, а дочерний процесс.

Этот дочерний процесс представляет собой всего лишь второй экземпляр той же программы. Однако, создав при запуске объектядра «событие", она обнаруживает, что такой объект уже существует. Тем самым она узнает, что является вторым экземпля ром, и выполняет другой код (отличный от того, который выполняется первым эк земпляром). Первое, что делает второй экземпляр, — вызывает DebugBreak:

VOID DebugBreak();