В. Гюго. Собор Парижской богоматери.
Книгу Майкла Дж. Янга "Visual C++ 6. Полное руководство" (BHV, 2000 г.) я купил в основном ради раздела, посвященного многопоточному программированию. И в очередной раз убедился, насколько опасно доверять привлекательной внешности: издание выглядит весьма солидно, хорошо оформлено (и цену, конечно, имеет соответствующую), а вот содержание...
Собственно, в заблуждение вводит уже название книги, в чем ее автор честно признается прямо во введении:
Можно спросить: "Как одна книга может быть исчерпывающим пособием по такому огромному продукту?" Это достигается [за счет того, что] ... вместо обсуждения деталей каждого компонента ... указываются названия разделов справочной системы для получения дополнительной информации.
Однако даже и это не совсем верно: по поводу интересовавшего меня многопоточного программирования я не нашел в книге никаких дополнительных указаний. Видимо, автор счел, что описал все достаточно подробно, - а зря.
Янг разбирает соответствующую технику на примере программы, отображающей на экране множество Мандельброта, строя сначала ее однопоточный вариант Mandel, а затем преобразуя его в многопоточный MandelMT, в котором обработка сообщений и вывод графики выполняются параллельно и независимо. Вы спросите: что же здесь плохого? А то, что в результате у Янга получилась чуть ли не антиреклама многопоточных средств и параллельных возможностей Visual C++ в среде Windows: попробовав запустить его пример, читатель, скорее всего, надолго утратит охоту к писанию параллельных программ.
Эксперимент с Mandel и MandelMT
По замыслу автора книги, обработка сообщений благодаря выделению в особый поток станет выполняться быстрее. Чтобы проверить, так ли это, я провел простейший тест, замерив для однопоточной Mandel и параллельной MandelIMT время реакции на выбор пункта New из меню File и на команду закрытия окна (очевидно, чем выше скорость обработки сообщений, тем это время должно быть короче).
Программа Mandel по каждой команде New немедленно начинала заново выводить рисунок, а при нажатии на крестик в углу окна в мгновение ока заканчивала работу. Если бы MandelMT реагировала еще быстрее, разницу трудно было бы заметить. Увы, она вела себя совершенно противоположным образом - полностью "уходила в себя" до окончания вывода изображения: указатель мыши замирал в виде "песочных часов", и, пока он не превращался в стрелку, не удавалось ни закрыть программу, ни начать новый рисунок.
Почти все примеры в книге базируются на однодокументном интерфейсе (SDI). Янг уверяет, что они подходят и для многодокументных (MDI) проектов, но в действительности это далеко не всегда так. Что касается данного примера, то программа Mandel с MDI-интерфейсом, естественно, выводила изображение только в активное окно, но реагировала по-прежнему без задержки, а MandelMT вела себя более чем странно. Она действительно рисовала во всех окнах, но не одновременно, а по очереди и вдобавок с ошибками, особенно заметными при нечетном числе окон. Склонность "тормозить", разумеется, сохранилась.
Короче говоря, многопоточная версия программы оказалась "менее параллельной", чем однопоточная. Я так и не понял, чем это было вызвано - трудноопределимой ошибкой в программе или несовершенством многопоточных средств Visual C++ и Windows. Но для читателя книги Янга вывод может быть только один: если не хочешь лишних проблем, от параллельных программ лучше держаться подальше.
Ниже я собираюсь предложить вашему вниманию вариант того же примера с заменой стандартных средств организации потоков на сетевую автоматную среду. Конечно, с точки зрения Windows программа останется однопоточной, но по внутренней структуре и по поведению будет параллельной. Как вы сможете убедиться, конечные автоматы не только не уступают по своим "параллельным" возможностям средствам Windows и Visual C++, но даже кое в чем превосходят их. Вдобавок, что очень важно, использование автоматов позволяет уменьшить число ошибок и сделать поведение программы более предсказуемым.
Для проверки приведенных программ вам понадобится библиотека FSA, обеспечивающая работу с автоматами, которую можно получить по адресу http://www.osp.ru/pcworld/2000/02/spfire.zip. Листинги сокращены; полный текст проекта в виде zip-файла находится по адресу http://www.osp.ru/pcworld/2001/02/???.
Создание автоматного проекта
- Запустите Visual C++.
- В меню File выберите пункт New, в открывшемся окне -- закладку Projects/MFC AppWizard (exe). Задайте имя проекта -- Mandel, и каталог, где он будет размещен, -- MastVC5EXAMPLES. (В данном случае мы создаем проект для Visual C++ 5, а не 6, как в книге, но существо дела от этого не страдает.)
- Нажмите кнопку Finish.
Корректировка установок и файлов проекта
- Откройте созданный проект.
- В меню Project выберите команду Settings, в открывшемся окне - закладку C/C++ и, наконец, в списке Category - пункт Preprocessor. В поле ввода Additional include directories введите lw_mc50nninclude. Нажмите OK.
- В окне Workspace выберите закладку FileView. Откройте папку Header Files, выберите файл StdAfx.h и добавьте в него строку #include .
- На той же странице FileView щелкните правой кнопкой мыши на корневой папке и выберите в открывшемся меню пункт Add Files to Project. Задайте тип файлов Library Files (.lib), папку lw_mc50nnSourceFsaFsam532Release и выберите файл fsam532.lib.
- Аналогичным образом включите в проект файл LwsLib.lib из каталога lw_mc50nnLwsLibDebug.
Изменения в классах CMandelApp и CMainFrame
Следующим шагом необходимо модифицировать автоматически генерируемые классы CMandelApp и CMainFrame. CMandelApp нужно сделать наследником класса CArrayNetFsa, реализующего сетевую автоматную среду. В нем же будет находиться основной цикл моделирования дискретного времени автоматной среды, для организации которого мы воспользуемся некоторыми особенностями функционирования метода OnIdle.
Изменения в CMainFrame нужны, чтобы обеспечить "непрерывность" дискретного времени. Дело в том, что в определенные моменты работы программы метод OnIdle класса CMandelApp не вызывается, и тогда за моделирование дискретного времени должен отвечать метод OnTimer класса CMainFrame.
Редактирование класса CMandelApp
- В окне проекта выберите закладку ClassView. Выберите класс CMandelApp.
- Отредактируйте заголовок класса, сделав его наследником класса CArrayNetFsa.
- Добавьте в класс метод OnIdle с помощью мастера ClassWizard. Откройте вкладку Message Maps. Установите в списках Class Name и Object IDs имя CMandelApp. Выберите в списке Messages пункт OnIdle, нажмите кнопку Add Function и затем OK.
В результате заголовок класса приобретет вид (Листинг 1)
В окне проекта ClassView выберите класс CMandelApp, затем метод OnIdle и отредактируйте его так, как показано (Листинге 2)
Редактирование класса CMainFrame
- Добавьте в класс обработку сообщений от таймера. Для этого, аналогично добавлению метода OnIdle для класса CMandelApp, введите в класс обработку сообщения WM_TIMER.
- Модифицируйте метод OnTimer следующим образом (Листинг 3)
- В метод OnCreate добавьте код инициализации таймера (Листинг 4)
Этим, собственно, и завершаются операции по превращению автоматически генерируемого типового проекта в автоматный.
Модификация класса отображения CMandelView
Для экспериментов с нашей автоматной программой подготовим дополнительный вариант класса CMandelView, изменив в нем метод DrawCol так, чтобы он возвращал управление методу OnIdle не после отрисовки очередного столбца, а после вывода каждого пиксела. В результате таких изменений изображение, естественно, станет выдаваться медленнее, зато скорость обработки сообщений должна возрасти - ведь в цикл обработки сообщений программа будет попадать чаще.
Чтобы реализовать этот план, необходимо, во-первых, позаботиться о запоминании от одного цикла прорисовки до другого значения переменной Row, которая содержит вертикальную координату пиксела, а во-вторых, исключить цикл for, заменив его, например, условным оператором. Для удобства сделаем переменную Row членом класса, переименовав ее, как того требует соглашение об именах переменных, в m_Row.
Измененный заголовок класса и новый вариант метода DrawCol показаны в Листинге 5
Автоматный класс отображения CMandelView
Итак, мы создали автоматный вариант проекта Mandel. Заметьте, что он, в отличие от исходного проекта из книги Янга, основан на MDI-интерфейсе, т. е. каждое открытое окно функционирует самостоятельно параллельно с другими отрытыми окнами объекта.
Перейдем теперь к реализации параллелизма. Чтобы сделать класс отображения аналогом самостоятельного потока в среде Windows, достаточно наделить его автоматными свойствами. Для этого выполните следующие шаги.
- Сделайте прикладной класс наследником автоматного класса LFsaAppl.
- Вставьте в него предикаты и действия.
- Создайте ссылку на основной объект программы - theApp.
- Включите в конструктор инициализацию базового класса LFsaAppl.
- Вставьте в конструктор код загрузки и инициализации автоматного процесса.
- Добавьте код таблицы переходов, коды предикатов и действий.
Автоматный вариант класса отображения показан в Листинге 6. Алгоритм его функционирования прост - бесконечный цикл выполнения действия y1. Это действие может содержать представленную в листинге 5 реализацию метода DrawCol. Действие y2 содержит вызов "эталонного" варианта метода DrawCol. Изменяя в таблице переходов действие y1 на y2 и наоборот, мы можем сравнивать работу разных методов вывода графики.
Предлагаемое автоматное решение абсолютно одинаково работает для SDI- и MDI-интерфейсов. Здесь действует "параллельный принцип": все, что может делать один объект, должен делать и другой, причем без каких-либо дополнительных усилий со стороны программиста. Он должен лишь проследить за тем, чтобы используемые объектом переменные были локальными для класса, - тогда работающие объекты будут надежно изолированы друг от друга.
Многопоточное автоматное программирование
Конечно, многопоточные средства Windows способны обеспечить более высокую скорость работы, чем параллельная автоматная модель, поскольку потоки поддерживаются на аппаратном уровне. Однако на фоне системных издержек Windows, связанных с обработкой сообщений и реализацией многооконного графического интерфейса, потери на автоматное моделирование весьма малы, так что в подавляющем большинстве случаев замедление почти не заметно. Кроме того, используя особенности конкретного автоматного алгоритма, потери почти всегда можно еще уменьшить.
Весьма важным достоинством сетевой автоматной среды является то, что она основана на изначально параллельной алгоритмической модели. Базовая несетевая автоматная модель программ также имеет ряд преимуществ перед обычной блок-схемной моделью.
Реализовать автоматную среду так, чтобы она стала многопоточной и в смысле ОС Windows, тоже возможно, но в этом пока не возникало необходимости: у большинства современных компьютеров все-таки по одному процессору.
Об авторе
С Вячеславом Селиверстовичем Любченко можно связаться по E-mail: slava@ivvson.kc.ru.