ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 30 от 28 января 2001 г.

Здравствуйте, дорогие друзья!

Я очень рад вас всех вновь приветствовать! К сожалению, по не зависящим от меня причинам я не имел возможности выпускать рассылку вплоть до настоящего времени. Искренне прошу извинить за причиненные неудобства и такой вот вынужденный перерыв. Хочу развеять опасения некоторых товарищей: рассылку я закрывать не собираюсь. Начиная с сегодняшнего дня, выпуски будут опять выходить регулярно.

За это время количество подписчиков перевалило за 10 000 - действительно круглое число! Создавая рассылку, я и не предполагал, что она будет пользоваться такой популярностью - все-таки весьма специфичная тематика. Но это значит, что рассылка актуальна, и это не может не радовать. Что еще могу сказать - оставайтесь с нами, и скорее всего не пожалеете! А теперь - let's get started!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Помнится, в одном из декабрьских выпусков шел у нас разговор о предотвращении запуска второй копии приложения. Тогда мы затронули тему использования объектов синхронизации, подробнее про которые я пообещал рассказать во второй части статьи про многозадачность. Тема эта хотя и очень интересная, но и довольно обширная; так что учитывая ограниченность места в одном выпуске, я освещу только самые важные для понимания моменты. Некоторые же второстепенные темы - такие, как предотвращение взаимного блокирования потоков, или оповещения об изменениях, - я здесь лишь упомяну, и (возможно) вынесу в дальнейшем в отдельную статью. Также в отдельную статью скорее всего выльется очень важная тема межпроцессного обмена данными (inter-process communication, IPC). Как скоро появятся эти статьи, будет зависеть от степени их востребованности. А пока представляю вашему вниманию давно обещанную вторую часть статьи про многозадачность.

Многозадачность и ее применение
Часть 2: Синхронизация потоков

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

Необходимость синхронизации
Если вы помните, в Windows выполняются не процессы, а потоки. При создании процесса автоматически создается его основной поток. Этот поток в процессе выполнения может создавать новые потоки, которые, в свою очередь, тоже могут создавать потоки и т.д. Процессорное время распределяется именно между потоками, и получается, что каждый поток работает независимо.

Все потоки, принадлежащие одному процессу, разделяют некоторые общие ресурсы - такие, как адресное пространство оперативной памяти или открытые файлы. Эти ресурсы принадлежат всему процессу, а значит, и каждому его потоку. Следовательно, каждый поток может работать с этими ресурсами без каких-либо ограничений. Но так ли это в действительности? Вспомним, что в Windows реализована вытесняющая многозадачность - это значит, что в любой момент система может прервать выполнение одного потока и передать управление другому. (Раньше использовался способ организации, называемый кооперативной многозадачностью. Система ждала, пока поток сам не соизволит передать ей управление. Именно поэтому в случае глухого зависания одного приложения приходилось перезагружать компьютер. Так была организована, например, Windows 3.1). Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс? Произойдет штука очень неприятная, я вам это могу с уверенностью сказать, и результат работы этих потоков может чрезвычайно сильно отличаться от задуманного. Такие конфликты могут возникнуть и между потоками, принадлежащими различным процессам. Всегда, когда два или более потоков используют какой-либо общий ресурс, возникает эта проблема.

Именно поэтому необходим механизм, позволяющий потокам согласовывать свою работу с общими ресурсами. Этот механизм получил название механизма синхронизации потоков (thread synchronization).

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

Объектов синхронизации существует несколько, самые важные из них - это взаимоисключение (mutex), критическая секция (critical section), событие (event) и семафор (semaphore). Каждый из этих объектов реализует свой способ синхронизации. Какой из них следует использовать в каждом конкретном случае вы поймете, подробно познакомившись с каждым из этих объектов. Также в качестве объектов синхронизации могут использоваться сами процессы и потоки (когда один поток ждет завершения другого потока или процесса); а также файлы, коммуникационные устройства, консольный ввод и уведомления об изменении (к сожалению, освещение этих объектов синхронизации выходит за рамки данной статьи).

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

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

Работа с объектами синхронизации
Чтобы создать тот или иной объект синхронизации, производится вызов специальной функции WinAPI типа Create... (напр. CreateMutex). Этот вызов возвращает дескриптор объекта (HANDLE), который может использоваться всеми потоками, принадлежащими данному процессу. Есть возможность получить доступ к объекту синхронизации из другого процесса - либо унаследовав дескриптор этого объекта, либо, что предпочтительнее, воспользовавшись вызовом функции открытия объекта (Open...). После этого вызова процесс получит дескриптор, который в дальнейшем можно использовать для работы с объектом. Объекту, если только он не предназначен для использования внутри одного процесса, обязательно присваивается имя. Имена всех объектов должны быть различны (даже если они разного типа). Нельзя, например, создать событие и семафор с одним и тем же именем.

По имеющемуся дескриптору объекта можно определить его текущее состояние. Это делается с помощью т.н. ожидающих функций. Чаще всего используется функция WaitForSingleObject. Эта функция принимает два параметра, первый из которых - дескриптор объекта, второй - время ожидания в мсек. Функция возвращает WAIT_OBJECT_0, если объект находится в сигнальном состоянии, WAIT_TIMEOUT - если истекло время ожидания, и WAIT_ABANDONED, если объект-взаимоисключение не был освобожден до того, как владеющий им поток завершился. Если время ожидания указано равным нулю, функция возвращает результат немедленно, в противном случае она ждет в течение указанного промежутка времени. В случае, если состояние объекта станет сигнальным до истечения этого времени, функция вернет WAIT_OBJECT_0, в противном случае функция вернет WAIT_TIMEOUT.
Если в качестве времени указана символическая константа INFINITE, то функция будет ждать неограниченно долго, пока состояние объекта не станет сигнальным.
Если необходимо узнавать о состоянии сразу нескольких объектов, следует воспользоваться функцией WaitForMultipleObjects.
Чтобы закончить работу с объектом и освободить дескриптор вызывается функция CloseHandle.

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

Теперь давайте рассмотрим каждый тип объектов синхронизации в отдельности.

Взаимоисключения
Объекты-взаимоисключения (мьютексы, mutex - от MUTual EXclusion) позволяют координировать взаимное исключение доступа к разделяемому ресурсу. Сигнальное состояние объекта (т.е. состояние "установлен") соответствует моменту времени, когда объект не принадлежит ни одному потоку и его можно "захватить". И наоборот, состояние "сброшен" (не сигнальное) соответствует моменту, когда какой-либо поток уже владеет этим объектом. Доступ к объекту разрешается, когда поток, владеющий объектом, освободит его.

Для того, чтобы объявить взаимоисключение принадлежащим текущему потоку, надо вызвать одну из ожидающих функций. Поток, которому принадлежит объект, может его "захватывать" повторно сколько угодно раз (это не приведет к самоблокировке), но столько же раз он должен будет его освобождать с помощью функции ReleaseMutex.

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

Функция CreateEvent создает объект-событие, SetEvent - устанавливает событие в сигнальное состояние, ResetEvent-сбрасывает событие. Функция PulseEvent устанавливает событие, а после возобновления ожидающих это событие потоков (всех при ручном сбросе и только одного при автоматическом), сбрасывает его. Если ожидающих потоков нет, PulseEvent просто сбрасывает событие.

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

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

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

Защищенный доступ к переменным
Существует ряд функций, позволяющих работать с глобальными переменными из всех потоков не заботясь о синхронизации, т.к. эти функции сами за ней следят. Это функции InterlockedIncrement/InterlockedDecrement, InterlockedExchange,InterlockedExchangeAdd и InterlockedCompareExchange. Например, функция InterlockedIncrement увеличивает значение 32-битной переменной на единицу - удобно использовать для различных счетчиков. Более подробно об этих функциях см. в документации.

Пример
Как известно, теория лучше всего познается на примерах. Давайте рассмотрим небольшой пример работы с объектом-взаимоисключением. Для простоты я использовал консольное Win32 приложение, но как вы понимаете, это совершенно не обязательно.

#include <windows.h>
#include <iostream.h>

void main()
{
	DWORD res;
	
	// создаем объект-взаимоисключение
	HANDLE mutex = CreateMutex(NULL, FALSE, "APPNAME-MTX01");
	// если он уже существует, CreateMutex вернет дескриптор существующего объекта,
	// а GetLastError вернет ERROR_ALREADY_EXISTS
	
	// в течение 20 секунд пытаемся захватить объект
	cout<<"Trying to get mutex...\n"; cout.flush();
	res = WaitForSingleObject(mutex,20000);

	if (res == WAIT_OBJECT_0) // если захват удался
	{
          // ждем 10 секунд
          cout<<"Got it! Waiting for 10 secs...\n"; cout.flush();
          Sleep(10000);
          
          // освобождаем объект
          cout<<"Now releasing the object.\n"; cout.flush();
          ReleaseMutex(mutex);
	}
	
	// закрываем дескриптор
	CloseHandle(mutex);
}

Для проверки работы мьютекса запустите сразу два экземпляра этого приложения. Первый экземпляр сразу захватит объект и освободит его только через 10 секунд. Только после этого второму экземпляру удастся захватить объект. В данном примере объект используется для синхронизации между процессами, поэтому он обятельно должен иметь имя.

Cинхронизация в MFC
Библиотека MFC содержит специальные классы для синхронизации потоков (CMutex, CEvent, CCriticalSection и CSemaphore). Эти классы соответствуют объектам синхронизации WinAPI и являются производными от класса CSyncObject. Чтобы понять, как их использовать, достаточно просто взглянуть на конструкторы и методы этих классов - Lock и Unlock. Фактически эти классы - всего лишь обертки для объектов синхронизации.

Eсть еще один способ использования этих классов - написание так называемых потоково-безопасных классов (thread-safe classes). Потоково-безопасный класс - это класс, представляющий какой либо ресурс в вашей программе. Вся работа с ресурсом осуществляется только через этот класс, который содержит все необходимые для этого методы. Причем класс спроектирован таким образом, что его методы сами заботятся о синхронизации, так что в приложении он используется как обычный класс. Объект синхронизации MFC добавляется в этот класс в качестве закрытого члена класса, и все функции этого класса, осуществляющие доступ к ресурсу, согласуют с ним свою работу.

С классами синхронизации MFC можно работать как напрямую, используя методы Lock и Unlock, так и через промежуточные классы CSingleLock и CMultiLock (хотя на мой взгляд, работать через промежуточные классы несколько неудобно. Но использование класса СMultiLock необходимо, если вы хотите следить за состоянием сразу нескольких объектов).

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

Тем, кто интересуется этой темой, могу порекомендовать следующие статьи и разделы MSDN:

  • Platform SDK / Windows Base Services / Executables / Processes and Threads
  • Platform SDK / Windows Base Services / Interprocess Communication / Synchronization
  • Periodicals 1996 / MSJ / December / First Aid For Thread-impaired:Using Multiple Threads with MFC
  • Periodicals 1996 / MSJ / March / Win32 Q&A
  • Periodicals 1997 / MSJ / July / C++ Q&A.
  • Periodicals 1997 / MSJ / January / Win32 Q&A.

    / / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

    Q| Как включать в проект незарегистрированный компонент ActiveX? Вернее он на моей машине зарегистрирован, а на другой нет, и в результате этого программа на той машине вообще не запускается. - Сергей Лобачев

    |A1 Большинство средств дистрибутирования (InstallShield, Wise, Windows Installer, etc.) позволяют регистрировать ActiveX-элементы в процессе инсталляции. При инсталляции "руками" можно вызвать regsvr32.exe и передать ей параметром исполняемый файл ActiveX-элемента. Если Вы сами пишете программу инсталляции - вызовите ф-ию DllRegisterServer из исполняемого файла ActiveX. Но при этом помните - для использования чужого ActiveX в коммерческих проектах необходимо иметь на то лицензию. - Andrew Shvydky
    |A2 Сначала необходимо учесть, что перед запуском программы на другом компьютере, в случае добавления в свой проект ActiveX (COM) компонентов,их необходимо будет перенести и зарегестрировать в реестре.
    Ответ несколькими способами:
    1. Первый И пожалуй самый надежный это сделать инсталяшку, которая будет заниматься, помимо установки, регистрацией ActiveX компонентов. (ActiveX надо включить в инсталяшку)
    2. Это написать .bat файл, в который включить строки regsvr32.exe my.ocx ... и принести на другой комп свой .exe, .ocx,и этот .bat файл, перед первым запуском запустить .bat который зарегистрирует твой ActiveX в системе, а далее запускай программу. (Стандартная программа Window regsvr32.exe, займется регистрацией ActiveX компонента в системе)
    3. Это самый утомительный, на другом компьютере через командную строку использую программу regsvr32.exe вручную зарегестрировать свои ActiveX компоненты. - Оleg Zhuk

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

    / / / / ОБРАТНАЯ СВЯЗЬ / / / / / / / / / / / / / /

    Хочу рассказать о решении одной проблемы, с которой я сам много провозился, да и многие другие тоже... Речь идет об инсталляции MSDN на компьютере, где уже установлен MSOffice 2000. Проблема возникает при регистрации коллекции справочников. Решение следующее: перенести файл C:\WINDOWS\HELP\HHCOLREG.DAT на другое место, а после установки MSDN объединить его с новым файлом на том же месте. Файл имеет простую текстовую структуру (XML) и разобраться в нем не составит труда. Другой вариант решения - ставить сначала MSDN, а уже затем Office. - Никита Зимин
    Прочитав дополнение Алексея Трошина к статье о DCOM по поводу реализации DCOM на платформах, отличных от Windows, решил внести и свою небольшую лепту. Дело в том, что существует, и уже довольно давно (в течении нескольких лет) реализация DCOM для нескольких платформ, включая различные варианты UNIX систем, IBM mainfraim и OpenVMS. Семейство продуктов носит название EntireX и реализовано это немецкой компанией Software AG. Более подробная информация есть на их сайте: http://www.softwareag.com/entirex/technical/data.htm
    Более того, эта же компания предоставляет бесплатную версию данного продукта для Linux, ее можно скачать отсюда: http://www.softwareag.com/entirex/download/free_download.htm Пакет включает в себя реализацию многих компонентов DCOM, вкючая подмножество Win32 API, Structured Storage, Automation, ATL версии 2.1 и др.
    Самое интересное, что все это даже работает :-) У нас был опыт успешного портирования Win32 DCOM сервиса, основанного на ATL под Linux платформу с использованием данного продукта.
    Одним из существенных недостатков данного продукта является цена версий для не-Linux платформ - нам ее, например, так ни разу и не назвали, наверное чтобы не отпугивать сразу :-), поскольку полагаю, она не меленькая.
    Прошу ни в коем случае не принимать мое письмо как рекламу данного продукта :-))) Я не имею никакого отношения к компании Software AG, просто подумал, что вам будет интересно об этом всем узнать. - Антон Масловский

    / / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

    Q| У меня одна проблема: Пишу одну программку (написал уже довольно много) используя Win32API. И у меня возникла проблема со ScrollBar'ами. Вся загвоздка в том, что позиция бегунка прокрутки описана как short int и соответственно лежит в двухбайтном диапазоне. А в моей программе диапазон прокрутки может быть больше чем 32767. В хелпе на сообщение WM_VSCROLL советуют использовать функцию GetScrollPos, у меня че-то не получилось ее использовать. Как решить эту проблему? - Алексей Иванов

       Ответить на вопрос

    / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

    Ну вот, на сегодня хватит. И так выпуски получаются довольно объемными. Кстати, хочу всем сказать: я НЕ высылаю архив выпусков по почте. Если вы хотите посмотреть старые выпуски, добро пожаловать в архив на Subscribe.ru.
    До встречи!

    Алекс Jenter   jenter@mail.ru
    Красноярск, 2001.

    Предыдущие выпуски     Статистика рассылки