2. Мультизадачность

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

В те времена, когда повсеместно наибольшей популярностью пользовалась однозадачная операционная система MS-DOS, пользователю была доступна так называемая переключательная мультизадачность XE "переключательная мультизадачность", основанная главном образом на резидентных программах. Резидентные программы калькуляторов позволяли, например, не прерывая работу программы редактора текста или другой программы, выполнить арифметические вычисления. Для переключения от выполнения основной задачи к работе с резидентной программой было нужно нажать ту или иную комбинацию клавиш.

К моменту появления мультизадачных операционных систем OS/2 и Windows было создано великое множество самых разнообразных и часто несовместимых между собой резидентных программ для MS-DOS. Среди них были достаточно мощные системы, такие, например, как Borland SideKick.

Появление операционной системы Microsoft Windows версии 3.0, работавшей как оболочка для MS-DOS, стимулировало появление приложений для Microsoft Windows, работавших в режиме невытесняющей мультизадачности XE "невытесняющая мультизадачность". При этом приложения, составленные определенным образом, время от времени передавали друг другу управление, в результате чего создавалась иллюзия одновременной работы нескольких приложений. Аналогичный принцип использовалася в сетевой операционной системе Novell NetWare и в компьютерах фирмы Apple.

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

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

Однако не следует думать, что у Microsoft не хватило ума организовать вытесняющую мультизадачность XE "вытесняющая мультизадачность", когда всем запущенным приложениям выделяются кванты времени с использованием системного таймера. Вытесняющая мультизадачность была использована в операционной системе OS/2 версий 1.0 - 1.3, которая в те времена разрабатывалась совместно Microsoft и IBM. Однако слабая архитектура процессора Intel 80286, недостаточная производительность выпускавшихся тогда компьютеров и малый объем оперативной памяти, установленной в компьютерах подавляющего числа пользователей (1 - 2 Мбайта) помешали широкому распространению OS/2. Эта операционная система с истинной вытесняющей мультизадачностью работала очень медленно и была вытеснена более легковесной оболочкой Microsoft Windows версии 3.1.

Сегодня ситуация изменилась. Современные операционные системы для персональных компьютеров, такие как Microsoft Windows 95, Microsoft Windows NT, IBM OS/2 Warp работают в режиме вытесняющей мультизадачности, когда все приложения гарантированно получают для себя кванты времени по прерыванию от таймера. При этом накладные расходы на мультизадачность компенсируются высокой производительностью компьютеров, поэтому пользователь не будет их чуствовать (конечно, если для уменьшения свопинга в компьютере установлено не менее 16 Мбайт оперативной памяти, что уже не редкость).

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

Процессы и задачи в Microsoft Windows NT

В операционной системе Microsoft Windows NT существуют два понятия, имеющие отношение к мультизадачности. Это процессы и задачи.

Процесс (process) создается, когда программа загружается в память для выполнения. Вы создаете процессы запуская, например, консольные прогрммы или графические приложения при помощи Program Manager. Как мы уже говорили, процессу выделяется в монопольное владение 2 Гбайта изолированного адресного пространства, в которое другие процессы не имеют никакого доступа.

Сразу после запуска процесса создается одна задача (thread), или, как ее еще называют в отечественной литературе, поток XE "поток". Задача - это просто фрагмент кода приложения, который может выполняться автономно и независимо от других задач в рамках одного процесса. Например, функция WinMain в приведенных нами ранее примерах приложений может рассматриваться как задача, которая запускается сразу после запуска процесса. При необходимости эта задача может запускать другие задачи, реализуя таким образом мультизадачность в рамках процесса. Все задачи имеют доступ к памяти, выделенной запустившему их процессу.

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

Распределение времени между задачами

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

Рис. 2.1. Квантование времени выполняется для задач

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

Для оптимальной работы системы необходимо правильно установить закон, по которому кванты времени выделяются задачам. В операционной системе Microsoft Windows NT используется приоритетное планирование задач, когда и процессы, и задачи имеют свои уровни приоритета. При необходимости операционная система может автоматически в небольших пределах изменять приоритеты, повышая, например, приоритет задач, активно работающих с периферийными устройствами компьютера.

Операционная система устанавливает уровень приоритета задач XE "уровень приоритета задач" в диапазоне от 1 до 31, причем значение 31 соответствует максимальному приоритету.

В процессе планирования кванты времени выделяются задачам с максимальным приоритетом. Менее приоритетные задачи получают управление только в том случае, если более приоритетные задачи переходят в состояние ожидания. Так как рано или поздно это обязательно происходит, то даже задачи с приоритетом, равным 1, имеют большие шансы получить кванты времени.

С другой стороны, если во время работы менее приоритеных задач запускается задача с более высоким приоритетом, все низкоприоритетные задачи приостанавливаются, а кванты времени выделяются более приоритетной задаче.

На первый взгляд может показаться странным, что приложения не могут устанавливать конкретное значение приоритета задач из указанного интервала значений. Вместо этого используется двухступенчатая система установки приоритетов для процессов и задач.

Классы приоритета процессов XE "классы приоритета процессов"

При запуске процесса с помощью функции CreateProcess XE "CreateProcess" (которая будет рассмотрена позже), ему можно назначить один из четырех классов приоритета:

Класс приоритета Уровень приоритета
REALTIME_PRIORITY_CLASS XE "REALTIME_PRIORITY_CLASS" 24 - процессы реального времени
HIGH_PRIORITY_CLASS XE "HIGH_PRIORITY_CLASS" 13 - высокоприоритетные процессы
NORMAL_PRIORITY_CLASS XE "NORMAL_PRIORITY_CLASS" 9 или 7 - обычные процессы
IDLE_PRIORITY_CLASS XE "IDLE_PRIORITY_CLASS" 4 - низкоприоритетные процессы

Когда приоритет процесса не указывается, то по умолчанию он получает приоритет класса NORMAL_PRIORITY_CLASS XE "NORMAL_PRIORITY_CLASS". Если это приложение работает в фоновом режиме, операционная система снижает его уровень приоритета до 7, если же окно приложения выдвигается на передний план - увеличивает до 9. Таким образом уровень приоритета приложения, с которым в данный момент работает пользователь, автоматически увеличивается.

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

В тех случаях, когда приложение должно немедленно отзываться на действия пользователя, ему, возможно, следует назначить класс приоритета HIGH_PRIORITY_CLASS XE "HIGH_PRIORITY_CLASS". Этот класс приоритета имеет, например, приложение Task Manager, с помощью которого пользователь может переключаться между запущенными приложениями а также завершать приложения. Не следует увлекаться созданием высокоприоритетных приложений, так как если в системе их будет запущенно много, то работа схемы, обеспечивающей оптимальный баланс производительности процессов, будет нарушена. В результате вы не получите желаемого эффекта.

Что же касается класса приоритета REALTIME_PRIORITY_CLASS XE "REALTIME_PRIORITY_CLASS", то он должен использоваться только системными процессами или драйверами. В противном случае будет блокирована работа клавиатуры и мыши, так как они обслуживаются с меньшим уровнем приоритета, чем приоритет классса REALTIME_PRIORITY_CLASS.

Относительный приоритет задач XE "относительный приоритет задач"

Как мы уже говорили, в рамках одного процесса может быть запущено несколько задач. Точно также как невозможно задать явным образом уровень приоритета процессов (лежащий в диапазоне значений от 1 до 31), невозможно задать и уровень приоритета задач, запущенных процессом. Вместо этого процесс при необходимости устанавливает функцией SetThreadPriority XE "SetThreadPriority" относительный приоритет задач, который может быть несколько ниже или выше приоритета процесса.

Указанной выше функции можно передать одно из следующих значений, определяющих новый приоритет задачи относительно приоритета процесса:

Значение Относительное изменение уровня приоритета
THREAD_PRIORITY_TIME_CRITICAL XE "THREAD_PRIORITY_TIME_CRITICAL" Устанавливается абсолютный уровень приоритета 15 или 31
THREAD_PRIORITY_HIGHEST XE "THREAD_PRIORITY_HIGHEST" +2
THREAD_PRIORITY_ABOVE_NORMAL XE "THREAD_PRIORITY_ABOVE_NORMAL" +1
THREAD_PRIORITY_NORMAL XE "THREAD_PRIORITY_NORMAL" 0
THREAD_PRIORITY_BELOW_NORMAL XE "THREAD_PRIORITY_BELOW_NORMAL" -1
THREAD_PRIORITY_LOWEST XE "THREAD_PRIORITY_LOWEST" -2
THREAD_PRIORITY_IDLE XE "THREAD_PRIORITY_IDLE" Устанавливается абсолютный уровень приоритета 1 или 16

Если процесс имеет класс приоритета, равный значению REALTIME_PRIORITY_CLASS XE "REALTIME_PRIORITY_CLASS", использование относительного приоритета THREAD_PRIORITY_TIME_CRITICAL XE "THREAD_PRIORITY_TIME_CRITICAL" приведет к тому, что уровень приоритета задачи будет равен 31. Если же это значение относительного приоритета укажет процесс более низкого класса приоритета, уровень приоритета задачи установится равным 15.

Для процесса с классом приоритета REALTIME_PRIORITY_CLASS XE "REALTIME_PRIORITY_CLASS" использование относительного приоритета THREAD_PRIORITY_IDLE XE "THREAD_PRIORITY_IDLE" приведет к тому, что будет установлен уровень приоритета задачи, равный 16. Если же значение THREAD_PRIORITY_IDLE будет использовано менее приоритетным процессом, уровень приоритета задачи будет равен 1.

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

Процесс может запустить задачу, а потом увеличить ее приоритет. В этом случае главная задача процесса, запустившая более приоритетную задачу, будет временно приостановлена. Если же процесс запустит задачу и уменьшит ее приоритет таким образом, что он станет меньше приоритета главной задачи процесса, будет приостановлена запущенная задача.

В составе Resource Kit for Windows NT XE "Resource Kit for Windows NT" и в составе SDK поставляется приложение Process Viewer XE "приложение Process Viewer" (рис. 2.2), пользуясь которым можно просмотреть и в некоторых случаях изменить приоритеты процессов и задач, а также получить другую информацию о запущенных задачах (использованное процессорное время с момента запуска, процент работы системного и пользовательского кода, использование виртуальной памяти и так далее).

Рис. 2.2. Приложение Process Viewer

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

Проблемы синхронизации задач и процессов

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

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

Специально для организации взаимодействия задач в операционной системе Microsoft Windows NT предусмотрены так называемые объекты синхронизации XE "объекты синхронизации". Это средства организации последовательного использования ресуросов (mutex), семафоры (semaphore) и события (event). Мы рассмотрим перечисленные средства позже в этой главе.

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

Передача данных между процессами и задачами

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

Что же делать?

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

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

Запуск задач

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

Во-первых, можно использовать фукнцию CreateThread XE "CreateThread", которая входит в программный интерфейс операционной системы Microsoft Windows NT. Этот способ предоставляет наибольшие возможности по управлению запущенными задачами, позволяя, в частности, присваивать запущенным задачам атрибуты защиты и создавать задачи в приостановленном состоянии.

Во-вторых, в вашем распоряжении имеется функция из библиотеки системы разработки Microsoft Visual C++ с названием _beginthread XE "_beginthread". Задачи, созданные с использованием этой функции, могут обращаться ко всем стандартным функциям библиотеки и к переменной errno XE "errno".

И, наконец, в-третьих, можно запустить задачу при помощи функции _beginthreadex XE "_beginthreadex", которая определена в библиотеке Microsoft Visual C++, но имеет возможности, аналогичные функции CreateThread XE "CreateThread".

Мы рассмотрим все эти способы.

Функция CreateThread XE "CreateThread"

Прототип функции CreateThread XE "CreateThread", с помощью которой процессы могут создавать задачи, представлен ниже:


HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,// атрибуты защиты 
  DWORD dwStackSize,       // начальный размер стека в байтах 
  LPTHREAD_START_ROUTINE lpStartAddress,// адрес функции 
                                        // задачи 
  LPVOID  lpParameter,           // параметры для задачи 
  DWORD   dwCreationFlags,       // параметры создания задачи 
  LPDWORD lpThreadId);           // адрес переменной для 
                                 // идентификатора задачи

Через параметр lpThreadAttributes передается адрес структуры SECURITY_ATTRIBUTES, определяющей атрибуты защиты для создаваемой задачи, или значение NULL. В последнем случае для задачи будут использованы атрибуты защиты, принятые по умолчанию. Это означает, что идентификатор созданной задачи можно использовать в любых функциях, выполняющих любые операции над задачами. Указывая атрибуты защиты, вы можете запретить использование тех или иных функций.

Приведем структуру SECURITY_ATTRIBUTES:


typedef struct _SECURITY_ATTRIBUTES 
{ 
  DWORD  nLength;              // размер структуры в байтах
  LPVOID lpSecurityDescriptor; // указатель на дескриптор 
                               //   защиты
  BOOL   bInheritHandle;       // флаг наследования 
                               //   идентификатора
} SECURITY_ATTRIBUTES;

При подготовке структуры в поле nLength следует записать размер структуры SECURITY_ATTRIBUTES XE "SECURITY_ATTRIBUTES".

Поле указателя на дескриптор защиты lpSecurityDescriptor не заполняется приложением непосредственно. Вместо этого для установки дескритора защиты используется набор функций, которым в качестве одного из параметров передается указатель на структуру SECURITY_ATTRIBUTES XE "SECURITY_ATTRIBUTES". Эти функции подробно описаны в SDK. В нашей книге для экономии места мы не будем на них останавливаться. Система защиты Microsoft Windows NT достаточно мощная и потому заслуживает отдельного рассмотрения.

Параметр dwStackSize функции CreateThread позволяет указать начальный размер стека для запускаемой задачи. Если указать для этого параметра нулевое значение, размер стека запущенной задачи будет равен размеру стека главной задачи процесса. При необходимости размер стека автоматически увеличивается.

Таким образом, первые два параметра функции CreateThread не вызывают затруднений. Они могут быть в большинстве случаев указаны как NULL и 0, соответственно.

Параметр lpStartAddress задает адрес функции, которая будет выполняться как отдельная задача. Здесь вы можете просто указать имя этой функции. Функция задачи имеет один 32-разрядный параметр и возвращает 32-разрядное значение. Указанный параметр передается функции CreateThread XE "CreateThread" через параметр lpParameter.

Если значение параметра dwCreationFlags равно нулю, после вызова функции CreateThread задача немедленно начнет свое выполнение. Если же в этом параметре указать значение CREATE_SUSPENDED XE "CREATE_SUSPENDED", задача будет загружена, но приостановлена. Возобновить выполнение приостановленной задачи можно будет позже с помощью функции ResumeThread XE "ResumeThread".

И, наконец, через параметр lpThreadId вы должны передать адрес переменной типа DWORD, в которую бедет записан системный номер созданной задачи (thread identifier).

В случае успеха функция CreateThread возвращает идентификатор задачи (thread handle), пользуясь которым можно выполнять над задачей те или иные операции. Не путайте этот идентификатор с системным номером задачи. При ошибке функция CreateThread возвращает значение NULL.

Ниже мы приведи пример использования функции CreateThread:


hThread = CreateThread(NULL, 0, 
  (LPTHREAD_START_ROUTINE)ThreadRoutine,
  (LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);

Здесь мы не используем атрибуты защиты и устанавливаем размер стека запускаемой задачи, равным размеру стека главной задачи процесса.

В качестве функции задачи мы использовали функцию с именем ThreadRoutine, имеющую следующий вид:


DWORD ThreadRoutine(HWND hwnd) 
{
 ...
  // Оператор return завершает выполнение задачи
  return 0;
}

Эта функция имеет один параметр, который в нашем случае будет принимать значение hwndChild.

Наша функция задачи завершает свое выполнение оператором return, хотя, как вы увидите дальше, есть и другие способы. Для проверки значения, возвращенного функцией задачи, процесс может воспользоваться функцией GetExitCodeThread XE "GetExitCodeThread", которая будет описана позже.

Так как при запуске задачи мы указали значение параметра dwCreationFlags, равное нулю, сразу после запуска задача начнет свою работу. Системный номер созданной задачи будет записан в переменную dwIDThread.

Функция _beginthread

Если вы создаете мультизадачное приложение и собираетесь при этом использовать функции стандартной библиотеки C, перед вами могут встать неожиданные проблемы. Дело в том, что первоначальные версии библиотек времени выполнения систем разработки не были рассчитаны на то, что в рамках одной программы могут существовать параллельно работающие задачи. В результате, например, могут возникнуть проблемы при использовании стандартных глобальных переменных, таких как errno, и функций, работающих с глобальными переменными.

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

Для решения этой проблемы в системе разработки Microsoft Visual C++ предусмотрены отдельные библиотеки для создания однозадачных и мультизадачных приложений.

Для выбора правильной библиотеки для проекта Microsoft Visual C++ версии 4.0 или 4.1 выберите из меню Build строку Settings. На экране появится блокнот настройки параметров проекта Project Settings. Открыв страницу C/C++ этого блокнота, выберите в списке Category строку Code Generation. После этого на странице появится список Use run-time library, в котором имеются следующие строки:

Если ваше приложение является однозадачным, имеет смысл выбрать библиотеку Single-Treaded или Debug Single-Treaded (при отладке). Функции из этой библиотеки будут работать быстрее, чем из библиотеки Multithreaded (которую тоже можно использовать в однозадачных приложениях), так как не будет накладных расходов на мультизадачность.

В том случае, если вы создаете мультизадачное приложение, необходимо использовать библиотеки Multithreaded и Multithreaded DLL (для создания мультизадачных библиотек DLL) либо отладочные версии этих библиотек.

Заметим, что для однозадачных приложений используется библиотека с именем libc.lib, а для мультизадачных - с именем libcmt.lib.

После того как вы указали мультизадачную библиотеку, вы можете использовать функции _beginthread XE "_beginthread" и _beginthreadex XE "_beginthreadex" для запуска задач. Приведем прототип функции _beginthread, описанный в файле process.h XE "process.h" :


unsigned long _beginthread(
  void(*StartAddress)(void*), // адрес функции задачи
  unsigned uStackSize,    // начальный размер стека в байтах
  void     *ArgList);     // параметры для задачи

Заметим, что функция задачи, запускаемой при помощи функции _beginthread XE "_beginthread", не возвращает никакого значения. Ее адрес передается функции _beginthread через параметр StartAddress. Через параметр ArgList вы можете передать функции задачи один параметр.

Начальный размер стека, выделяемого задаче, указывается через параметр uStackSize. Так же как и в случае с функцией CreateThread XE "CreateThread", для размера стека можно указать нулевое значение. При этом для задачи создается стек такого же размера, что и для главной задачи процесса.

В случае успеха функция _beginthread XE "_beginthread" возвращает идентификатор запущенной задачи. Если же произошла ошибка, возвращается значение -1.

Приведем пример использования функции _beginthread XE "_beginthread" :


_beginthread(ThreadRoutine, 0, (void*)(Param));

Здесь запускается задача, функция которой имеет имя ThreadRoutine. Ей передается в качестве параметра значение Param.

Функция ThreadRoutine должна выглядеть следующим образом:


void ThreadRoutine(void *Param) 
{
 ...
  _endthread();
}

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

Функция _beginthreadex

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


unsigned long _beginthreadex(
  void     *Security,       // указатель на дескриптор защиты
  unsigned StackSize,              // начальный размер стека
  unsigned (*StartAddress)(void*), // адрес функции задачи
  void     *ArgList,             // параметры для задачи
  unsigned Initflag,             // параметры создания задачи
  unsigned *ThrdAddr);           // адрес созданной задачи

Для запуска задачи в приостановленном состоянии через параметр Initflag необходимо передать значение CREATE_SUSPENDED XE "CREATE_SUSPENDED".

Функция задачи, которая запускается с помощью функции _beginthread ex, имеет один параметр и возвращает 32-разрядное значение, аналогично функции задачи, запускаемой функцией CreateThread XE "CreateThread". Для завершения своего выполнения функция задачи должна использовать либо оператор возврата, либо функцию _endthreadex, не имеющую параметров.

В случае успеха функция _beginthread ex возвращает идентификатор запущенной задачи. Если же произошла ошибка, возвращается значение 0 (а не -1, как это было для функции _beginthread).

Управление запущенными задачами

После того как задача запущена, запустившая задача знает идентификатор дочерней задачи. Этот идентификатор возвращается функциями CreateThread XE "CreateThread", _beginthread XE "_beginthread" и _beginthreadex XE "_beginthreadex". Пользуясь этим идентификатором, запустившая задача может управлять состоянием дочерней задачи, изменяя ее приоритет, приостанавливая, возобновляя или завершая ее работу.

Изменение приоритета задачи

В разделе “Относительный приоритет задач” нашей книги мы рассказали вам о том, как в операционной системе Microsoft Windows NT устанавливаются приоритеты задач. Родительская задача может изменить относительный приоритет запущенной ей дочерней задачи с помощью функции SetThreadPriority XE "SetThreadPriority" :


BOOL SetThreadPriority(
  HANDLE hThread,    // идентификатор задачи 
  int    nPriority); // новый уровень приоритета задачи

Через параметр hThread этой функции передается идентификатор задачи, для которой необходимо изменить относительный приоритет.

Новое значение относительного приоритета передается через параметр nPriority и может принимать одно из следующих значений:

Абсолютный уровень приоритета, который получит задача, зависит от класса приоритета процесса. Забегая вперед, скажем, что класс приоритета процесса можно изменить при помощи функции SetPriorityClass XE "SetPriorityClass".

Определение приоритета задачи

Зная идентификатор задачи, нетрудно определить ее относительный приоритет. Для этого следует воспользоваться функцией GetThreadPriority XE "GetThreadPriority" :


int GetThreadPriority(HANDLE hThread);

Эта функция возвращает одно из значений, перечисленных выше в разделе “Изменение приоритета задачи” или значение THREAD_PRIORITY_ERROR_RETURN XE "THREAD_PRIORITY_ERROR_RETURN" при возникновении ошибки.

Приостановка и возобновление выполнения задачи

В некоторых случаях имеет смысл приостановить выполнение задачи. Например, если пользователь работает с многооконным приложением, и для каждого окна запускается отдельная задача, для повышения производительности можно приостановить выполнение задач в неактивных окнах.

Приостановка выполнения задачи выполняется с помощью функции SuspendThread XE "SuspendThread" :


DWORD SuspendThread(HANDLE hThread);

Через единственный параметр этой функции нужно передать идентификатор приостанавливаемой задачи.

Для каждой задачи операционная система хранит счетчик приостановок, который увеличивается при каждом вызове фукнции SuspendThread XE "SuspendThread". Если значение этого счетчика больше нуля, задача приостанавливается.

Для уменьшения значения счетчика приостановок и, соответственно, для возобновления выполнения задачи вы должны использовать фукнцию ResumeThread XE "ResumeThread" :


DWORD ResumeThread(HANDLE hThread);

В случае ошибки функции SuspendThread и ResumeThread возвращают значение 0xFFFFFFFF.

Временная приостановка работы задачи

С помощью функции Sleep задача может приостановить свою работу на заданный период времени:


VOID Sleep(DWORD cMilliseconds); // время в миллисекундах

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

Если для времени ожидания указать нулевой значение, то при наличии в системе запущенных задач с таким же приоритетом, как и у задачи, вызвавшей эту функцию, ожидающая задача отдает оставшееся время от своего кванта другой задаче. В том случае, когда в системе нет других задач с таким же значением приоритета, функция Sleep XE "Sleep" немедленно возвращает управление вызвавшей ее задаче, которая сама будет использовать остаток своего кванта времени.

Есть еще одна возможность: можно организовать бесконечную задержку, передав функции Sleep XE "Sleep" значение INFINITE XE "INFINITE".

Завершение задачи

Задача может завершиться как по собственной инициативе, так и по инициативе другой задачи. В первом случае задача либо выполняет оператор возврата из функции задачи, либо пользуется специальными функциями.

Для того чтобы завершить свое выполнение, задача, запущенная с помощью фукнции CreateThread XE "CreateThread", может вызвать функцию ExitThread XE "ExitThread", передав ей код завершения:


VOID ExitThread(DWORD dwExitCode);

Для принудительного завершения дочерней задачи родительская задача может использовать функцию TerminateThread XE "TerminateThread", передав ей идентификатор завершаемой задачи и код завершения:


BOOL TerminateThread(
  HANDLE hThread,     // идентификатор завершаемой задачи 
  DWORD  dwExitCode); // код завершения 

Как родительская задача может получить код завершения дочерней задачи?

Для этого она должна вызвать фукнцию GetExitCodeThread:


BOOL GetExitCodeThread(
  HANDLE  hThread,     // идентификатор завершаемой задачи 
  LPDWORD lpExitCode); // адрес для приема кода завершения

Если задача, для которой вызвана фукнция GetExitCodeThread, все еще работает, вместо кода завершения возвращается значение STILL_ACTIVE.

Как мы уже говорили, если задача была запущена с помощью функций _beginthread или _beginthreadex, она может завершать свое выполнение только с помощью функций, соответственно, _endthread и _endthreadex. Функцию ExitThread XE "ExitThread" в этом случае использовать нельзя, так как при этом не будут освобождены ресурсы, заказанные для работы с мультизадачным вариантом библиотеки времени выполнения.

Освобождение идентификатора задачи

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

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

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

Приложение MultiSDI

Наше первое мультизадачное приложение называется MultiSDI. В его главном окне (рис. 2.3) рисуют четыре задачи, одна из которых является главной и соответствует функции WinMain.

Рис. 2.3. Главное окно приложения MultiSDI

Немного позже мы приведем исходные тексты MDI-приложения MultiMDI, в котором для каждого дочернего MDI-окна создается отдельная задача.

Главная задача приложения MultiSDI рисует в главном окне приложения во время обработки сообщения WM_PAINT. Кроме того, она создает еще три задачи, которые рисуют в окне приложения, соответственно, эллипсы, прямоугольники и текстовую строку TEXT. Цвет и размеры фигур, а также цвет текстовой строки и цвет фона, на котором отображается эта строка, выбираются случайным образом, поэтому содержимое главного окна изменяется хаотически. Это выглядит достоточно забавно.

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

Исходные тексты приложения

Главный файл исходных текстов приложения MultiSDI представлен в листинге 2.1. Заметим, что для сборки проекта мультизадачного приложения необходимо использовать мультизадачный вариант библиотеки времени выполнения. Об этом мы уже говорили в разделе “Функция _beginthread XE "_beginthread" ”.

Листинг 2.1. Файл multisdi/multisdi.c


#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <process.h>
#include <stdio.h>
#include "resource.h"
#include "afxres.h"
#include "multisdi.h"

HINSTANCE hInst;
char szAppName[]  = "MultiSDI";
char szAppTitle[] = "Multithread SDI Application";

// Критическая секция для рисования в окне
CRITICAL_SECTION csWindowPaint; 

// Признак завершения всех задач
BOOL fTerminate = FALSE;

// Массив идентификаторов запущенных задач
HANDLE hThreads[3];

// -----------------------------------------------------
// Функция WinMain
// -----------------------------------------------------
int APIENTRY 
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
{
  WNDCLASSEX wc;
  HWND hWnd;
  MSG msg;
  
  // Сохраняем идентификатор приложения
  hInst = hInstance;

  // Преверяем, не было ли это приложение запущено ранее
  hWnd = FindWindow(szAppName, NULL);
  if(hWnd)
  {
    // Если было, выдвигаем окно приложения на
    // передний план
    if(IsIconic(hWnd))
      ShowWindow(hWnd, SW_RESTORE);
    SetForegroundWindow(hWnd);
    return FALSE;
  }

  // Регистрируем класс окна
  memset(&wc, 0, sizeof(wc));
  wc.cbSize = sizeof(WNDCLASSEX);
  wc.hIconSm = LoadImage(hInst,
    MAKEINTRESOURCE(IDI_APPICONSM), 
    IMAGE_ICON, 16, 16, 0);
  wc.style = 0;
  wc.lpfnWndProc = (WNDPROC)WndProc;
  wc.cbClsExtra  = 0;
  wc.cbWndExtra  = 0;
  wc.hInstance = hInst;
  wc.hIcon = LoadImage(hInst,
    MAKEINTRESOURCE(IDI_APPICON), 
    IMAGE_ICON, 32, 32, 0);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
  wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU);
  wc.lpszClassName = szAppName;
  if(!RegisterClassEx(&wc))
    if(!RegisterClass((LPWNDCLASS)&wc.style))
	  return FALSE;
    
  // Создаем главное окно приложения
  hWnd = CreateWindow(szAppName, szAppTitle, 
     WS_OVERLAPPEDWINDOW, 
     CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 
     NULL, NULL, hInst, NULL);
  if(!hWnd) return(FALSE);

  // Отображаем окно и запускаем цикл 
  // обработки сообщений
  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);
  while(GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// -----------------------------------------------------
// Функция WndProc
// -----------------------------------------------------
LRESULT WINAPI
WndProc(HWND hWnd, UINT msg, WPARAM wParam, 
        LPARAM lParam)
{
  switch(msg)
  {
    HANDLE_MSG(hWnd, WM_CREATE,     WndProc_OnCreate);
    HANDLE_MSG(hWnd, WM_DESTROY,    WndProc_OnDestroy);
    HANDLE_MSG(hWnd, WM_PAINT,      WndProc_OnPaint);
    HANDLE_MSG(hWnd, WM_COMMAND,    WndProc_OnCommand);

    default:
      return(DefWindowProc(hWnd, msg, wParam, lParam));
  }
}

// -----------------------------------------------------
// Функция WndProc_OnCreate
// -----------------------------------------------------
BOOL WndProc_OnCreate(HWND hWnd, 
                      LPCREATESTRUCT lpCreateStruct)
{
  // Инициализируем критическую секцию
  InitializeCriticalSection(&csWindowPaint);

  // Сбрасываем флаг завершения задач
  fTerminate = FALSE;

  // Запускаем три задачи, сохраняя их идентификаторы
  // в массиве
  hThreads[0] = (HANDLE)_beginthread(PaintEllipse, 
    0, (void*)hWnd);
  hThreads[1] = (HANDLE)_beginthread(PaintRect, 
    0, (void*)hWnd);
  hThreads[2] = (HANDLE)_beginthread(PaintText, 
    0, (void*)hWnd);

  return TRUE;
}

// -----------------------------------------------------
// Функция WndProc_OnDestroy
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnDestroy(HWND hWnd)
{
  // Устанавливаем флаг завершения задач
  fTerminate = TRUE;

  // Дожидаемся завершения всех трех задач
  WaitForMultipleObjects(3, hThreads, TRUE, INFINITE);
  
  // Перед удаляем критическую секцию
  DeleteCriticalSection(&csWindowPaint);
  
  // Останавливаем цикл обработки сообщений, расположенный 
  // в главной задаче
  PostQuitMessage(0);
  return 0L;
}

// -----------------------------------------------------
// Функция WndProc_OnPaint
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnPaint(HWND hWnd)
{
  HDC hdc;
  PAINTSTRUCT ps;
  RECT rc;

  // Входим в критическую секцию
  EnterCriticalSection(&csWindowPaint);

  // Перерисовываем внутреннюю область окна
  hdc = BeginPaint(hWnd, &ps);
  
  GetClientRect(hWnd, &rc);
  DrawText(hdc, "SDI Window", -1, &rc,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);

  EndPaint(hWnd, &ps);

  // Выходим из критической секции
  LeaveCriticalSection(&csWindowPaint);
  return 0;
}
// -----------------------------------------------------
// Функция WndProc_OnCommand
// -----------------------------------------------------
#pragma warning(disable: 4098)
void WndProc_OnCommand(HWND hWnd, int id, 
  HWND hwndCtl, UINT codeNotify)
{
  switch (id)
  {
    case ID_FILE_EXIT:  
    {
      // Завершаем работу приложения
      PostQuitMessage(0);
      return 0L;
      break;
    }
	  
    case ID_HELP_ABOUT:
    {
      MessageBox(hWnd, 
        "Multithread SDI Application\n"
        "(C) Alexandr Frolov, 1996\n"
        "Email: frolov@glas.apc.org",
        szAppTitle, MB_OK | MB_ICONINFORMATION);
      return 0L;
      break;
    }

    default:
      break;
  }
  return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify,
    DefWindowProc);
}

// -----------------------------------------------------
// Функция задачи PaintEllipse
// -----------------------------------------------------
void PaintEllipse(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;

  // Инициализация генератора случайных чисел
  srand((unsigned int)hwnd);

  // Задача работает в бесконечном цикле,
  // который будет прерван при завершении приложения
  while(!fTerminate) 
  {
    // Входим в критическую секцию
    EnterCriticalSection(&csWindowPaint);

    // Отображаем эллипс, имеющий случайный цвет,
    // форму и расположение
    
    // Получаем контекст отображения
    hDC = GetDC(hwnd);

    // Получаем случайные цветовые компоненты
    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    
    // Получаем случйные размеры эллипса
    GetWindowRect(hwnd, &rect);
    
    xLeft   = rand() % (rect.left   + 1);
    xRight  = rand() % (rect.right  + 1);
    yTop    = rand() % (rect.top    + 1);
    yBottom = rand() % (rect.bottom + 1);

    // Создаем кисть на основе случайных цветов
    hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
    
    // Выбираем кисть в контекст отображения
    hOldBrush = SelectObject(hDC, hBrush);

    // Рисуем эллипс
    Ellipse(hDC, min(xLeft, xRight), min(yTop, yBottom),
                 max(xLeft, xRight), max(yTop, yBottom));

    // Выбираем старую кисть в контекст отображения
    SelectObject(hDC, hOldBrush);

    // Удаляем созданную кисть
    DeleteObject(hBrush);

    // Освобождаем контекст отображения
    ReleaseDC(hwnd, hDC);
    
    // Выходим из критической секции
    LeaveCriticalSection(&csWindowPaint);

    // Выполняем задержку на 500 мс
    Sleep(500);
  }
}

// -----------------------------------------------------
// Функция задачи PaintRect
// -----------------------------------------------------
void PaintRect(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;

  srand((unsigned int)hwnd + 1);

  while(!fTerminate) 
  {
    EnterCriticalSection(&csWindowPaint);

    hDC = GetDC(hwnd);

    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    
    GetWindowRect(hwnd, &rect);
    
    xLeft   = rand() % (rect.left   + 1);
    xRight  = rand() % (rect.right  + 1);
    yTop    = rand() % (rect.top    + 1);
    yBottom = rand() % (rect.bottom + 1);

    hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
    hOldBrush = SelectObject(hDC, hBrush);

    Rectangle(hDC, min(xLeft, xRight), min(yTop, yBottom),
                 max(xLeft, xRight), max(yTop, yBottom));

    SelectObject(hDC, hOldBrush);
    DeleteObject(hBrush);
    ReleaseDC(hwnd, hDC);
    
    LeaveCriticalSection(&csWindowPaint);
    Sleep(500);
  }
}

// -----------------------------------------------------
// Функция задачи PaintText
// -----------------------------------------------------
void PaintText(void *hwnd)
{
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;

  srand((unsigned int)hwnd + 2);

  while(!fTerminate) 
  {
    EnterCriticalSection(&csWindowPaint);

    hDC = GetDC(hwnd);

    GetWindowRect(hwnd, &rect);
    
    xLeft   = rand() % (rect.left   + 1);
    xRight  = rand() % (rect.right  + 1);
    yTop    = rand() % (rect.top    + 1);
    yBottom = rand() % (rect.bottom + 1);

    // Устанавливаем случайный цвет текста
    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    SetTextColor(hDC, RGB(nRed, nGreen, nBlue));

    // Устанавливаем случайный цвет фона
    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    SetBkColor(hDC, RGB(nRed, nGreen, nBlue));

    TextOut(hDC, xRight - xLeft,
      yBottom - yTop,"TEXT", 4);

    ReleaseDC(hwnd, hDC);
    
    LeaveCriticalSection(&csWindowPaint);
    Sleep(500);
  }
}

Файл multisdi.h (листинг 2.2) содержит прототипы функций, определенных в приложении MultiSDI. Это функция главного окна приложения WndProc, функции обработки сообщений WM_CREATE XE "WM_CREATE", WM_DESTROY XE "WM_DESTROY", WM_PAINT XE "WM_PAINT", WM_COMMAND XE "WM_COMMAND" (с именами, соответственно, WndProc_OnCreate, WndProc_OnDestroy, WndProc_OnPaint и WndProc_OnCommand), а также функции задач PaintEllipse, PaintRect и PaintText.

Листинг 2.2. Файл multisdi/multisdi.h


// -----------------------------------------------------
// Описание функций
// -----------------------------------------------------
LRESULT WINAPI
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct);
void WndProc_OnDestroy(HWND hWnd);
void WndProc_OnPaint(HWND hWnd);
void WndProc_OnCommand(HWND hWnd, int id, 
  HWND hwndCtl, UINT codeNotify);

void PaintEllipse(void *hwnd);
void PaintRect(void *hwnd);
void PaintText(void *hwnd);

Файл resource.h (листинг 2.3) создается автоматически и содержит определения констант для файла описания ресурсов приложения.

Листинг 2.3. Файл multisdi/resource.h


//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by MultiSDI.RC
//
#define IDR_APPMENU                     102
#define IDI_APPICON                     103
#define IDI_APPICONSM                   104
#define ID_FILE_EXIT                    40001
#define ID_HELP_ABOUT                   40003

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        121
#define _APS_NEXT_COMMAND_VALUE         40004
#define _APS_NEXT_CONTROL_VALUE         1000
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

В файле описания ресурсов приложения MultiSDI (листинг 2.4), который тоже создается автоматически, определено главное меню приложения IDR_APPMENU и две пиктограммы (стандартного и уменьшенного размера), а также таблица строк, которая не используется.

Листинг 2.4. Файл multisdi/multisdi.rc


//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

//////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

//////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

//////////////////////////////////////////////////////////////
// Menu
//

IDR_APPMENU MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "E&xit",                       ID_FILE_EXIT
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&About...",                   ID_HELP_ABOUT
    END
END

#ifdef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

//////////////////////////////////////////////////////////////
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APPICON             ICON    DISCARDABLE     "multisdi.ico"
IDI_APPICONSM           ICON    DISCARDABLE     "multissm.ico"

//////////////////////////////////////////////////////////////
// String Table
//

STRINGTABLE DISCARDABLE 
BEGIN
    ID_FILE_EXIT            "Quits the application"
END

#endif    // English (U.S.) resources
//////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 3 resource.
//
//////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

Определения и глобальные переменные

Помимо обычных include-файлов, характерных для наших приложений, в приложении MultiSDI используется файл process.h. Этот файл содержит прототип функции _beginthread XE "_beginthread", с помощью которой наше приложение создает задачи.

Для синхронизации задач, выполняющих рисование в окне приложения, мы определили глобальную структуру csWindowPaint типа CRITICAL_SECTION, которая будет использоваться в качестве критической секции:


CRITICAL_SECTION csWindowPaint; 

В глобальной переменной fTerminate хранится флаг, установка которого в состояние TRUE вызывает завершение всех трех задач рисования.

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

Описание функций

Приведем описание функций, определенных в нашем приложении.

Функция WinMain

Функция WinMain не имеет никаких особенностей. Сразу после того как она получит управление, функция проверяет, не было ли данное приложение уже запущено. Если было, окно запущенного приложения выдвигается на передний план.

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

Функция WndProc

Функция главного окна приложения WndProc обрабатывает сообщения WM_CREATE, WM_DESTROY XE "WM_DESTROY", WM_PAINT и WM_COMMNAD. Для этого с помощью макрокоманды HANDLE_MSG она вызывает, соответственно, функции WndProc_OnCreate, WndProc_OnDestroy, WndProc_OnPaint и WndProc_OnCommand.

Необработанные сообщения передаются фукнции DefWindowProc.

Функция WndProc_OnCreate

При создании главного окна приложения функция WndProc_OnCreate инициализирует структуру критической секции csWindowPaint, что необходимо для использования этого средства синхронизации задач:


InitializeCriticalSection(&csWindowPaint);

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

Последнее, что делает функция WndProc_OnCreate перед возвращением управления, это запуск задач PaintEllipse, PaintRect и PaintText. Для запуска в приложении MultiSDI мы использовали функцию _beginthread XE "_beginthread" :


hThreads[0] = (HANDLE)_beginthread(PaintEllipse, 0,
  (void*)hWnd);
hThreads[1] = (HANDLE)_beginthread(PaintRect,    0, 
  (void*)hWnd);
hThreads[2] = (HANDLE)_beginthread(PaintText,    0, 
  (void*)hWnd);

Через первый параметр мы передаем фукнции _beginthread имя функции задачи.

Второй параметр, определяющий начальный размер стека задачи, равен нулю, поэтому этот размер равен начальному размеру стека главной задачи приложения.

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

В приложении с многооконным интерфейсом MultiMDI мы воспользуемся другой функцией предназначенной для запуска задач - функцией CreateThread XE "CreateThread".

Функция WndProc_OnDestroy

Когда наше приложение завершает свою работу, функция WndProc_OnDestroy, обрабатывающая сообщение WM_DESTROY XE "WM_DESTROY", устанавливает содержимое глобальной переменной fTerminate в состояние TRUE. Функции задач периодически проверяют эту переменную, и как только ее содержимое станет равным TRUE, они завершают свою работу.

Так как в нашем приложении была создана критическая секция, ее следует удалить перед завершением приложения. Однако прежде чем это сделать, необходимо убедиться, что все фукнции задач завершили свою работу. Это можно сделать с помощью функции WaitForMultipleObjects XE "WaitForMultipleObjects", которая будет описана позже в разделе, посвященном синхронизации задач:


WaitForMultipleObjects(3, hThreads, TRUE, INFINITE);

В качестве первого параметра этой функции передается количество задач. Через второй параметр передается адрес массива с идентификаторами этих задач. Третий параметр, имеющий значение TRUE, указывает, что нужно дождаться завершения именно всех задач, а не одной из них. И, наконец, последний параметр определяет, что ожидание может длиться неограниченно долго.

Как только все три задачи завершат свою работу, функция WndProc_OnDestroy удалит критическую секцию с помощью функции DeleteCriticalSection XE "DeleteCriticalSection" :


DeleteCriticalSection(&csWindowPaint);

После этого вызывается функция PostQuitMessage, в результате чего завершается цикл обработки сообщений.

Функция WndProc_OnPaint

Функция WndProc_OnPaint выполняет очень простую вещь - она рисует в центре главного окна приложения текстовую строку SDI Window, используя для этого известную вам из программирования для Microsoft Windows версиии 3.1 функцию DrawText XE "DrawText".

Обратите внимание, что перед получением контекста отображения мы входим в критическую секцию, вызывая функцию EnterCriticalSection XE "EnterCriticalSection". После завершения рисования выполняется выход из критической секции с помощью фукнции LeaveCriticalSection XE "LeaveCriticalSection" :


EnterCriticalSection(&csWindowPaint);
hdc = BeginPaint(hWnd, &ps);
GetClientRect(hWnd, &rc);
DrawText(hdc, "SDI Window", -1, &rc, 
  DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hWnd, &ps);
LeaveCriticalSection(&csWindowPaint);

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

Почему это важно?

Дело в том, что для повышения производительности графическая система Microsoft Windows NT не содержит средств синхронизации задач. Поэтому для того чтобы функции графического интерфейса работали правильно, такую синхронизацию должно выполнять само приложение.

Функция WndProc_OnCommand

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

Функция задачи PaintEllipse

Функция задачи PaintEllipse (наряду с другими двумя функциями задач PaintRect и PaintText) запускается при помощи функции _createthread во время инициализации главного окна приложения.

Прежде всего функция PaintEllipse выполняет инициализацию генератора случайных чисел, передавая функции инициализации srand в качестве начального значения идентификатор окна приложения. Вы можете, разумеется, использовать здесь любое другое 32-разрядное значение.

Затем функция PaintEllipse входит в цикл, который будет завершен при установке глобальной переменной fTerminate в состояние TRUE. Это произойдет при уничтожении главного окна приложения.

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

Способ рисования эллипса не имеет никаких особенностей. Соответствующая функция Ellipse была нами описана в 14 томе “Библиотеки системного программиста”, который называется “Графичский интерфейс GDI в MS Windows”.

После того как эллипс будет нарисован, мы покидаем критическую секцию, вызывая функцию LeaveCriticalSection, и с помощью функции Sleep XE "Sleep" выполняем небольшую задержку. Функция Sleep приостанавливает выполнение задачи на количество миллисекунд, указанное в единственном параметере. Во время задержки задача не получает квантов времени, поэтому ожидание, выполняемое с помощью фукнции Sleep, не снижает производительности системы.

Функция задачи PaintRect

Функция задачи PaintRect аналогична только что рассмотренной функции PaintEllipse, за исключением того что она рисует прямоугольники. Для инициализации генератора случайных чисел используется другое значение, а рисование прямоугольника выполняется функцией Rectangle XE "Rectangle". Эта функция имеет такие же параметры, что и функция Ellipse.

Для синхронизации задача PaintRect использует все ту же критическую секцию csWindowPaint.

Функция задачи PaintText

Задача PaintText рисует текстовую строку TEXT, используя для этого функцию TextOut, описанную в 11 томе “Библиотеки системного программсита”. Синхронизация задачи выполняется с помощью критической секции csWindowPaint.

Приложение MultiMDI

В предыдущем разделе мы привели исходные тексты однооконного мультизадачного приложения. Больший интерес, на наш взгляд, имеет создание многооконного MDI-приложения XE "MDI-приложение", в котором для каждого окна создается своя задача. В качестве шаблона для создания такого приложения вы можете взять исходные тексты приложения MultiMDI, которые мы приведем в этом разделе.

В главном окне приложения MultiMDI вы можете создать дочерние MDI-окна, в которых выполняется циклическое отображение эллипсов случайной формы и цвета (рис. 2.4).

Рис. 2.4. Мультизадачное MDI-приложение MultiMDI

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

С помощью меню Priotitry class, показанном на рис. 2.5, вы можете устанавливать один из четырех классов приоритета текущего процесса.

Рис. 2.5. Меню Priotitry class для установки приоритета процесса

Если сделать щелчок правой клавишей мыши во внутренней области дочернего MDI-окна, на экране появится плавающее меню, с помощью которого можно управлять задачей, запущенной для данного окна. Это меню показано на рис. 2.6.

Рис. 2.6. Плавающее меню, предназначенное для управления задачей, запущенной для MDI-окна

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

Изменяя относительный приоритет отдельных окон, вы можете заметить, что скорость перерисовки эллипсов также изменяется. Если вы работаете на быстром компьютере, эффект будет заметнее, если создать не менее 20 - 30 дочерних MDI-окон.

Выбрав строку Get Priority, вы можете узнать текущий относительный приоритет для любого дочернего MDI-окна. Значение относительного приоритета будет показано в отдельной диалоговой панели (рис. 2.7).

Рис. 2.7. Просмотр относительного приоритета задачи, запущенной для MDI-окна

Выбрав строку Kill Thread, вы принудительно завершите работу соответствующей задачи, после чего она не будет отзываться на любые команды.

С помощью строки Close Window вы можете закрыть любой дочернее MDI-окно, в том числе то, для которого было выполнено принудительное завершение работы задачи. Дочернее окно можно также закрыть, сделав двойной щелчок левой клавишей мыши по системному меню дочернего окна. В меню Window есть строка Close all, позволяющая закрыть сразу все дочерние MDI-окна.

Исходные тексты приложения

В качестве прототипа для создания исходных текстов приложения MultiMDI мы взяли исходные тексты приложения MDIAPP, описанного в 17 томе “Библиотеки системного программиста”, который называется “Microsoft Windows 3.1 для программиста. Дополнительные главы”. В этом томе описаны принципы работы MDI-приложений, которые в Microsoft Windows NT остались такими же, что и в Microsoft Windows версии 3.1.

В те фрагменты кода, которые выполняют создание дочерних MDI-окон, мы внесли небольшие изменения, связанные с использованием мультизадачного режима работы. В частности, для создания MDI-окна мы использовали функцию CreateMDIWindow, которая, как сказано в документации SDK, позволяет создавать для дочерних окон отдельные задачи.

Кроме того, для обеспечения необходимой синхронизации главной задачи и задач, запущенных для дочерних MDI-окон, мы создаем критические секции (по одной для каждого дочернего MDI-окна).

Главный файл исходных текстов приложения MultiMDI приведен в листинге 2.5.

Листинг 2.5. Файл multimdi/multimdi.c


#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <stdio.h>
#include "resource.h"
#include "afxres.h"
#include "multimdi.h"

// Имена классов окна
char const szFrameClassName[] = "MDIAppClass";
char const szChildClassName[] = "MDIChildAppClass";

// Заголовок окна
char const szWindowTitle[] = "Multithread MDI Application";

HINSTANCE hInst;

HWND hwndFrame;  // окно Frame Window
HWND hwndClient; // окно Client Window
HWND hwndChild;  // окно Child Window

// Структура, которая создается для каждого дочернего окна
typedef struct _CHILD_WINDOW_TAG
{
  // Признак активности задачи
  BOOL fActive;

  // Критическая секция для рисования в окне
  CRITICAL_SECTION csChildWindowPaint; 

  // Идентификатор задачи
  HANDLE hThread;
} CHILD_WINDOW_TAG;

typedef CHILD_WINDOW_TAG *LPCHILD_WINDOW_TAG;

// =====================================
// Функция WinMain
// =====================================
int APIENTRY 
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        LPSTR lpCmdLine, int nCmdShow)
{
  MSG  msg;          // структура для работы с сообщениями
  hInst = hInstance; // сохраняем идентификатор приложения

  // Инициализируем приложение
  if(!InitApp(hInstance))
    return FALSE;

  // Создаем главное окно приложения - Frame Window
  hwndFrame = CreateWindow(
    szFrameClassName,    // имя класса окна
    szWindowTitle,       // заголовок окна
    WS_OVERLAPPEDWINDOW, // стиль окна
    CW_USEDEFAULT, 0,    // задаем размеры и расположение
    CW_USEDEFAULT, 0,    // окна, принятые по умолчанию
    0,                   // идентификатор родительского окна
    0,                   // идентификатор меню
    hInstance,           // идентификатор приложения
    NULL);   // указатель на дополнительные параметры

  // Если создать окно не удалось, завершаем приложение
  if(!hwndFrame)
    return FALSE;

  // Рисуем главное окно
  ShowWindow(hwndFrame, nCmdShow);
  UpdateWindow(hwndFrame);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, NULL, 0, 0))
  {
    // Трансляция для MDI-приложения
    if(!TranslateMDISysAccel(hwndClient, &msg))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// Выполняет регистрацию класса окна
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации

  // Регистрируем класс для главного окна приложения
  // (для окна Frame Window)
  memset(&wc, 0, sizeof(wc));
  wc.lpszMenuName  = "APP_MENU";
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC)FrameWndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(hInstance, "APP_ICON");
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
  wc.lpszClassName = (LPSTR)szFrameClassName;
  aWndClass = RegisterClass(&wc);

  if(!aWndClass)
    return FALSE;

  // Регистрируем класс окна для 
  //дочернего окна Document Window
  memset(&wc, 0, sizeof(wc));
  wc.lpszMenuName  = 0;
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC)ChildWndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(hInstance, "APPCLIENT_ICON");
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszClassName = (LPSTR)szChildClassName;
  aWndClass = RegisterClass(&wc);

  if(!aWndClass)
    return FALSE;

  return TRUE;
}

// =====================================
// Функция FrameWndProc
// =====================================

LRESULT CALLBACK 
FrameWndProc(HWND hwnd, UINT msg, 
             WPARAM wParam, LPARAM lParam)
{
  HWND   hwndChild;
  HANDLE hThread;
  DWORD  dwIDThread;
  CHAR   szBuf[255];
  
  // Структура для создания окна Client Window
  CLIENTCREATESTRUCT clcs;

  // Указатель на структуру для хранения 
  // состояния дочернего окна
  LPCHILD_WINDOW_TAG lpTag;

  switch (msg)
  {
    // При создании окна Frame Window создаем
    // окно Client Window, внутри которого будут создаваться
    // дочерние окна Document Window
    case WM_CREATE:
    {
      // Получаем и сохраняем в структуре clcs идентификатор
      // временного меню Window. Так как это третье слева 
      // меню, его позиция равна 2 (меню File имеет позицию 0) 
      clcs.hWindowMenu = GetSubMenu(GetMenu(hwnd), 2);

      // Идентификатор первого дочернего окна Document Window
      clcs.idFirstChild = 500;
      
      // Создаем окно Client Window 
      hwndClient = CreateWindow(
        "MDICLIENT",    // имя класса окна
        NULL,           // заголовок окна
        WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | 
          WS_HSCROLL | WS_VSCROLL,  
        0, 0, 0, 0,
        hwnd,           // идентификатор родительского окна
        (HMENU)1,       // идентификатор меню
        hInst,          // идентификатор приложения
        (LPSTR)&clcs);// указатель на дополнительные параметры

      break;
    }

    // Обработка сообщений от главного меню приложения
    case WM_COMMAND:
    {
      switch (wParam)
      {
        // Создание нового окна Document Window
        case CM_FILENEW:
        {
          // Используем функцию CreateMDIWindow, которая
          // специально предназначена для работы с
          // многозадачными приложениями MDI
          hwndChild = CreateMDIWindow(
            (LPSTR)szChildClassName,      // класс окна
            "MDI Child Window",           // заголовок окна
            0,                      // дополнительные стили
            CW_USEDEFAULT, CW_USEDEFAULT, // размеры окна 
            CW_USEDEFAULT, CW_USEDEFAULT, // Document Window
            hwndClient,  // идентификатор окна Client Window
            hInst,       // идентификатор приложения
            0);          // произвольное значение

          // Получаем память для структуры, в которой будет
          // хранится состояние окна
          lpTag = malloc(sizeof(CHILD_WINDOW_TAG));
  
          // Устанавливаем признак активности
          lpTag->fActive = 1;

          // Инициализируем критическую секцию
          InitializeCriticalSection(
             &(lpTag->csChildWindowPaint));
          
          // Устанавливаем адрес структуры состояния в
          // памяти окна
          SetWindowLong(hwndChild, GWL_USERDATA, (LONG)lpTag);

          // Создаем задачу для дочернего окна
          hThread = CreateThread(NULL, 0, 
            (LPTHREAD_START_ROUTINE)ThreadRoutine,
            (LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);

          if(hThread == NULL)
          {
            MessageBox(hwnd,"Ошибка при создании задачи",
	           szWindowTitle, MB_OK | MB_ICONEXCLAMATION);
          }

          // Сохраняем идентификатор созданной задачи
          lpTag->hThread = hThread;
          
          // Отображаем идентификатор задачи в заголовке
          // дочернего окна
          sprintf(szBuf, "Thread ID = %lX", dwIDThread);
          SetWindowText(hwndChild, szBuf);
   
          break;
        }

        // Размещение окон Document Window рядом друг с другом
        case CM_WINDOWTILE:
        {
          SendMessage(hwndClient, WM_MDITILE, 0, 0);
          break;
        }

        // Размещение окон Document Window с перекрытием
        case CM_WINDOWCASCADE:
        {
          SendMessage(hwndClient, WM_MDICASCADE, 0, 0);
          break;
        }

        // Размещение пиктограм минимизированых окон 
        // Document Window в нижней части окна Client Window
        case CM_WINDOWICONS:
        {
          SendMessage(hwndClient, WM_MDIICONARRANGE, 0, 0);
          break;
        }

        // Уничтожение всех окон Document Window
        case CM_WINDOWCLOSEALL:
        {
          HWND hwndTemp;

          // Скрываем окно Client Window для того чтобы 
          // избежать многократной перерисовки окон 
          // Document Window во время их уничтожения
          ShowWindow(hwndClient, SW_HIDE);

          while(TRUE) 
          {
            // Получаем идентификатор дочернего окна 
            // для окна Client Window
            hwndTemp = GetWindow(hwndClient, GW_CHILD);
            // Если дочерних окон больше нет, выходим из цикла
            if(!hwndTemp)
              break;

            // Пропускаем окна-заголовки
            while(hwndTemp && GetWindow(hwndTemp, GW_OWNER))
              hwndTemp = GetWindow(hwndTemp, GW_HWNDNEXT);

            // Удаляем дочернее окно Document Window
            if(hwndTemp)
            {
              // Завершаем задачу, запущенную для окна
              lpTag = (LPCHILD_WINDOW_TAG)GetWindowLong(
                 hwndTemp, GWL_USERDATA);
              lpTag->fActive = 0;  

              SendMessage(hwndClient, WM_MDIDESTROY, 
                (WPARAM)hwndTemp, 0);
            }
            else
              break;
          }

          // Отображаем окно Client Window
          ShowWindow(hwndClient, SW_SHOW);
          break;
        }

        // Устанавливаем классы приоритета процесса
        case ID_PRIORITYCLASS_REALTIME:
        {
          SetPriorityClass(GetCurrentProcess(),
            REALTIME_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_HIGH:
        {
          SetPriorityClass(GetCurrentProcess(),
            HIGH_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_NORMAL:
        {
          SetPriorityClass(GetCurrentProcess(),
            NORMAL_PRIORITY_CLASS);
          break;
        }
        case ID_PRIORITYCLASS_IDLE:
        {
          SetPriorityClass(GetCurrentProcess(),
            IDLE_PRIORITY_CLASS);
          break;
        }

        case CM_HELPABOUT:
        {
          MessageBox(hwnd,
	    "Демонстрация использования мультизадачности\n"
            "в MDI-приложениях\n"
            "(C) Alexandr Frolov, 1996\n"
            "Email: frolov@glas.apc.org",
            szWindowTitle, MB_OK | MB_ICONINFORMATION);
          break;
        }

        // Завершаем работу приложения
        case CM_FILEEXIT:
        {
          DestroyWindow(hwnd);
          break;
        }

        default:
          break;
      }

      // Определяем идентификатор активного окна 
      // Document Window
      hwndChild =
        (HWND)LOWORD(SendMessage(hwndClient, 
        WM_MDIGETACTIVE, 0, 0l));

      // Если это окно, посылаем ему сообщение WM_COMMAND
      if(IsWindow(hwndChild))
        SendMessage(hwndChild, WM_COMMAND, wParam, lParam);

      return DefFrameProc(
         hwnd, hwndClient, msg, wParam, lParam);
    }

    case WM_DESTROY:
    {
      PostQuitMessage(0);
      break;
    }

    default:
      break;
  }
  return DefFrameProc(hwnd, hwndClient, msg, wParam, lParam);
}

// =====================================
// Функция ChildWndProc
// =====================================

LRESULT CALLBACK
ChildWndProc(HWND hwnd, UINT msg, 
             WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  RECT rc;
  LPCHILD_WINDOW_TAG lpMyWndTag;
  HMENU hmenuPopup;
  POINT pt;
  CHAR szBuf[256];

  switch (msg)
  {
    case WM_PAINT:
    {
      // Получаем адрес структуры состояния окна
      lpMyWndTag = 
        (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

      // Входим в критическую секцию
      EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));

      // Перерисовываем внутреннюю область дочернего окна
      hdc = BeginPaint(hwnd, &ps);
      GetClientRect(hwnd, &rc);

      DrawText(hdc, "Child Window", -1, &rc,
        DT_SINGLELINE | DT_CENTER | DT_VCENTER);

      EndPaint(hwnd, &ps);

      // Выходим из критической секции
      LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
      break;
    }

    case WM_CLOSE:
    {
      // Сбрасываем признак активности задачи
      lpMyWndTag = 
        (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

      lpMyWndTag->fActive = 0;  
      break;
    }

    // Когда пользователь нажимает правую кнопку мыши 
    // в дочернем окне, отображаем плавающее меню
    case WM_RBUTTONDOWN:
    {
      pt.x = LOWORD(lParam);
      pt.y = HIWORD(lParam);
      ClientToScreen(hwnd, &pt);

      hmenuPopup = GetSubMenu(
        LoadMenu(hInst, "IDR_POPUPMENU"), 0);
      TrackPopupMenu(hmenuPopup, 
        TPM_CENTERALIGN | TPM_LEFTBUTTON,
        pt.x, pt.y, 0, hwnd, NULL);
      DestroyMenu(hmenuPopup);
      break;
    }

    // Обрабатываем команды, поступающие от плавающего меню
    case WM_COMMAND:
    {
      switch (wParam)
      {
        // Приостановка выполнения задачи
        case ID_THREADCONTROL_SUSPEND:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
             hwnd, GWL_USERDATA);

          // Входим в критическую секцию
          EnterCriticalSection(
            &(lpMyWndTag->csChildWindowPaint));

          SuspendThread(lpMyWndTag->hThread);

          // Выходим из критической секции
          LeaveCriticalSection(
            &(lpMyWndTag->csChildWindowPaint));
          break;
        }
        
        // Возобновление выполнения задачи
        case ID_THREADCONTROL_RESUME:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          ResumeThread(lpMyWndTag->hThread);
          break;
        }

        // Изменение относительного приоритета
        case ID_THREADCONTROL_PRIORITYLOWEST:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_LOWEST);
          break;
        }
        case ID_THREADCONTROL_PRIORITYNORMAL:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_NORMAL);
          break;
        }
        case ID_THREADCONTROL_PRIORITYHIGHEST:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          SetThreadPriority(lpMyWndTag->hThread, 
            THREAD_PRIORITY_HIGHEST);
          break;
        }

        // Определение и отображение относительного приоритета
        case ID_THREADCONTROL_GETPRIORITY:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          
          strcpy(szBuf, "Thread priority: ");
          
          switch (GetThreadPriority(lpMyWndTag->hThread))
          {
            case THREAD_PRIORITY_LOWEST:
            {
              strcat(szBuf, "THREAD_PRIORITY_LOWEST");
              break;
            }
            case THREAD_PRIORITY_BELOW_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_BELOW_NORMAL");
              break;
            }
            case THREAD_PRIORITY_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_NORMAL");
              break;
            }
            case THREAD_PRIORITY_ABOVE_NORMAL:
            {
              strcat(szBuf, "THREAD_PRIORITY_ABOVE_NORMAL");
              break;
            }
            case THREAD_PRIORITY_HIGHEST:
            {
              strcat(szBuf, "THREAD_PRIORITY_HIGHEST");
              break;
            }
          }

          MessageBox(hwnd, szBuf,
            szWindowTitle, MB_OK | MB_ICONINFORMATION);
          
          break;
        }

        // Удаление задачи
        case ID_THREADCONTROL_KILLTHREAD:
        {
          lpMyWndTag = 
            (LPCHILD_WINDOW_TAG)GetWindowLong(
            hwnd, GWL_USERDATA);
          TerminateThread(lpMyWndTag->hThread, 5);
          break;
        }
        
        // Уничтожение дочернего окна
        case ID_THREADCONTROL_CLOSEWINDOW:
        {
          SendMessage(hwndClient, WM_MDIDESTROY, 
            (WPARAM)hwnd, 0);
          break;
        }
        default:
          break;
      }
      break;
    }
    default:
      break;
  }
  return DefMDIChildProc(hwnd, msg, wParam, lParam);
}

// =====================================
// Функция ThreadRoutine
// Задача, которая выполняется для каждого
// дочернего окна
// =====================================

DWORD ThreadRoutine(HWND hwnd) 
{
  LONG lThreadWorking = 1L;
  HDC hDC;
  RECT rect;
  LONG xLeft, xRight, yTop, yBottom;
  short nRed, nGreen, nBlue;
  HBRUSH hBrush, hOldBrush;
  LPCHILD_WINDOW_TAG lpMyWndTag;

  // Определение адреса структуры, содержащей состояние
  // дочернего окна
  lpMyWndTag = 
    (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

  // Инициализация генератора случайных чисел
  srand((unsigned int)hwnd);

  // Задача работает в бесконечном цикле,
  // который будет прерван при удалении дочернего окна
  while(TRUE) 
  {
    // Получаем признак активности задачи
    // Если он не равен 1, завершаем задачу
    if(!lpMyWndTag->fActive)
      break;

    // Входим в критическую секцию
    EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));

    // Отображаем эллипс, имеющий случайный цвет,
    // форму и расположение
    
    // Получаем контекст отображения
    hDC = GetDC(hwnd);

    // Получаем случайные цветовые компоненты
    nRed   = rand() % 255;
    nGreen = rand() % 255;
    nBlue  = rand() % 255;
    
    // Получаем случйные размеры эллипса
    GetWindowRect(hwnd, &rect);
    
    xLeft   = rand() % (rect.left   + 1);
    xRight  = rand() % (rect.right  + 1);
    yTop    = rand() % (rect.top    + 1);
    yBottom = rand() % (rect.bottom + 1);

    // Создаем кисть на основе случайных цветов
    hBrush = CreateSolidBrush(RGB(nRed, nGreen, nBlue));
    
    // Выбираем кисть в контекст отображения
    hOldBrush = SelectObject(hDC, hBrush);

    // Рисуем эллипс
    Ellipse(hDC, min(xLeft, xRight), min(yTop, yBottom),
                 max(xLeft, xRight), max(yTop, yBottom));

    // Выбираем старую кисть в контекст отображения
    SelectObject(hDC, hOldBrush);

    // Удаляем созданную кисть
    DeleteObject(hBrush);

    // Освобождаем контекст отображения
    ReleaseDC(hwnd, hDC);
    
    // Выходим из критической секции
    LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));

    // Выполняем задержку на 1 мс
    Sleep(1);
  }

  // Перед обычным завершением задачи удаляем критическую
  // секцию и освобождаем память, полученную для
  // хранения состояния дочернего окна
  DeleteCriticalSection(&(lpMyWndTag->csChildWindowPaint));
  free(lpMyWndTag);
  
  // Оператор return завершает выполнение задачи
  return 0;
}

Прототипы функций, определенных в приложении MultiMDI, определены в файле multimdi.h (листинг 2.6).

Листинг 2.6. Файл multimdi/multimdi.h


// Прототипы функций
BOOL InitApp(HINSTANCE);
LRESULT CALLBACK FrameWndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK ChildWndProc(HWND, UINT, WPARAM, LPARAM);
DWORD ThreadRoutine(HWND hwnd);
VOID AddThreadToList(HANDLE hThread);

Файл resource.h (листинг 2.7), создаваемый автоматически, содержит определение констант для ресурсов приложения MultiMDI.

Листинг 2.7. Файл multimdi/resource.h


//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by MDIAPP.RC
//
#define CM_HELPABOUT                    100
#define CM_FILEEXIT                     101
#define CM_FILENEW                      102
#define CM_WINDOWTILE                   103
#define CM_WINDOWCASCADE                104
#define CM_WINDOWICONS                  105
#define CM_WINDOWCLOSEALL               106
#define ID_THREADCONTROL_SUSPEND        40001
#define ID_THREADCONTROL_RESUME         40002
#define ID_THREADCONTROL_PRIORITYLOWEST 40003
#define ID_THREADCONTROL_PRIORITYNORMAL 40004
#define ID_THREADCONTROL_PRIORITYHIGHEST 40005
#define ID_THREADCONTROL_GETPRIORITY    40006
#define ID_THREADCONTROL_KILLTHREAD     40007
#define ID_THREADCONTROL_CLOSEWINDOW    40008
#define ID_PRIORITYCLASS_REALTIME       40009
#define ID_PRIORITYCLASS_NORMAL         40010
#define ID_PRIORITYCLASS_HIGH           40011
#define ID_PRIORITYCLASS_IDLE           40012

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC                     1
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40013
#define _APS_NEXT_CONTROL_VALUE         1000
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

Ресурсы приложения MultiMDI определены в файле mdiapp.rc, который приведен в листинге 2.8.

Листинг 2.8. Файл multimdi/mdiapp.rc


//Microsoft Developer Studio generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

//////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

//////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

//////////////////////////////////////////////////////////////
// Menu
//

APP_MENU MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New",           CM_FILENEW
        MENUITEM SEPARATOR
        MENUITEM "E&xit",          CM_FILEEXIT
    END
    POPUP "Priority class"
    BEGIN
        MENUITEM "&Realtime",      ID_PRIORITYCLASS_REALTIME
        MENUITEM "&High",          ID_PRIORITYCLASS_HIGH
        MENUITEM "&Normal",        ID_PRIORITYCLASS_NORMAL
        MENUITEM "&Idle",          ID_PRIORITYCLASS_IDLE
    END
    POPUP "&Window"
    BEGIN
        MENUITEM "&Tile",          CM_WINDOWTILE
        MENUITEM "&Cascade",       CM_WINDOWCASCADE
        MENUITEM "Arrange &Icons", CM_WINDOWICONS
        MENUITEM "Close &All",     CM_WINDOWCLOSEALL
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&About...",      CM_HELPABOUT
    END
END

IDR_POPUPMENU MENU DISCARDABLE 
BEGIN
    POPUP "&Thread Control"
    BEGIN
        MENUITEM "&Suspend",         ID_THREADCONTROL_SUSPEND
        MENUITEM "&Resume",          ID_THREADCONTROL_RESUME
        MENUITEM SEPARATOR
        MENUITEM "Priority &Lowest", ID_THREADCONTROL_PRIORITYLOWEST
        MENUITEM "Priority &Normal", ID_THREADCONTROL_PRIORITYNORMAL
        MENUITEM "Priority &Highest",ID_THREADCONTROL_PRIORITYHIGHEST
        MENUITEM SEPARATOR
        MENUITEM "Get Priority",    ID_THREADCONTROL_GETPRIORITY
        MENUITEM SEPARATOR
        MENUITEM "&Kill Thread",     ID_THREADCONTROL_KILLTHREAD
        MENUITEM "&Close Window",    ID_THREADCONTROL_CLOSEWINDOW
    END
END

//////////////////////////////////////////////////////////////
// Icon
//

// Icon with lowest ID value placed first to ensure 
// application icon
// remains consistent on all systems.
APP_ICON                ICON    DISCARDABLE     "multimdi.ico"
APPCLIENT_ICON          ICON    DISCARDABLE     "mdicl.ico"

#ifdef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
//////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
//////////////////////////////////////////////////////////////
// Generated from the TEXTINCLUDE 3 resource.
//
//////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

Определения и глобальные переменные

Так как для запуска задач в приложении MultiMDI мы пользуемся функцией CreateThread, нам не нужно включать файл process.h.

В глобальных переменных szFrameClassName и szChildClassName хранятся указатели, соответственно, на имена классов для окна Frame Window и окна Child Window.

Адрес заголовка приложения хранится в глобальной переменной szWindowTitle.

Переменная hInst используется для хранения идентификатора приложения.

Переменные hwndFrame, hwndClient и hwndChild используются, соответственно, для хранения идентификаторов окон Frame Window, Client Window и дочернего окна (в момент его создания).

Для каждого дочернего MDI-окна мы создаем структуру типа CHILD_WINDOW_TAG, в которой сохраняем такую информацию, как признак активности задачи, запущенной для этого окна, критическую секцию для рисования в окне, а также идентификатор задачи, запущенной для окна:


typedef struct _CHILD_WINDOW_TAG
{
  BOOL fActive;
  CRITICAL_SECTION csChildWindowPaint; 
  HANDLE hThread;
} CHILD_WINDOW_TAG;

Кроме того, мы определили указатель на эту структуру:


typedef CHILD_WINDOW_TAG *LPCHILD_WINDOW_TAG;

Описание функций приложения

Приведем описание функций, определенных в нашем приложении. Для экономии места мы не будем подробно описывать фрагменты кода, связанные с созданием дочерних MDI-окон. При необходимости обращайтесь к 17 тому “Библиотеки системного программиста”, в котором приведена подробная информация о создании MDI-приложений.

Функция WinMain

Функция WinMain характерна для MDI-приложений. В ней вызывается функция инициализации приложения InitApp, которая регистрирует классы главного окна Frame Window и дочернего MDI-окна Document Window. Затем создается и отображается главное окно, а затем запускается цикл обработки сообщений. В этом цикле вызывается функция трансляции TranslateMDISysAccel, котурую используют MDI-приложения.

Функция FrameWndProc

Эта функция обрабатывает сообщения для главного окна приложения Frame Window, в частности, сообщения, которые поступают от главного меню приложения.

При создании окна функция FrameWndProc получает сообщение WM_CREATE. В ответ на это сообщение она сохраняет в структуре clcs типа CLIENTCREATESTRUCT XE "CLIENTCREATESTRUCT" идентификатор временного меню Window. При создании дочерних MDI-окон это меню будет расширяться.

Затем обработчик сообщения WM_CREATE сохраняет в структуре clcs идентификатор первого дочернего MDI-окна, для которого выбрано произвольное число 500. После этого выполняется создание дочернего MDI-окна, для чего вызывается функция CreateWindow. Описание этой функции вы найдете в 11 томе “Библиотеки системного программиста”.

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

Создание нового дочернего окна

При выборе из меню File строки New создается новое дочернее MDI-окно и задача для него. Первое действие выполняется с помощью функции CreateMDIWindow, специально созданной для мультизадачных MDI-приложений. Задача создается при помощи функции CreateThread XE "CreateThread". Рассмотрим процесс создания дочернего окна подробнее.

Параметры фукнции CreateMDIWindow являются комбинацией значений, сохраняемых в структуре MDICREATESTRUCT XE "MDICREATESTRUCT" полюс идентификатор окна Client Window. Напомним, что адрес этой структуры передается через второй параметр сообщения WM_MDICREATE XE "WM_MDICREATE", предназначенного для создания дочерних MDI-окон в однозадачных приложениях. Вот как создается такое окно в нашем приложении:


hwndChild = CreateMDIWindow(
  LPSTR)szChildClassName,        // класс окна
  "MDI Child Window",            // заголовок окна
  0,                             // дополнительные стили
  CW_USEDEFAULT, CW_USEDEFAULT,  // размеры окна 
  CW_USEDEFAULT, CW_USEDEFAULT,  //    Document Window
  hwndClient,            // идентификатор окна Client Window
  hInst,                         // идентификатор приложения
  0);                            // произвольное значение

Вы можете сравнить это со способом, описанным нами в 17 томе “Библиотеки системного программиста” в разделе “Создание и уничтожение окна Document Window”.

Для каждого дочернего окна мы создаем и инициализируем структуру типа CHILD_WINDOW_TAG, содержащую такие значения, как идентификатор задачи, запущенной для окна, признак активности задачи и критическую секцию. Память для структуры мы получаем простейшим способом - с помощью функции malloc XE "malloc" :


lpTag = malloc(sizeof(CHILD_WINDOW_TAG));

Далее в процессе инициализации этой структуры мы устанавливаем начальное знзачение для признака активности и выполняем инициализацию критической секции:


lpTag->fActive = 1;
InitializeCriticalSection(&(lpTag->csChildWindowPaint));

Это нужно сделать обязательно до запуска задачи, которая будет пользоваться критической секцией и проверять флаг активности.

Адрес структуры мы сохраняем в области данных окна, используя для этого функцию SetWindowLong XE "SetWindowLong" :


SetWindowLong(hwndChild, GWL_USERDATA, (LONG)lpTag);

Впоследствии этот адрес будет извлечен функцией задачи.

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


hThread = CreateThread(NULL, 0, 
  (LPTHREAD_START_ROUTINE)ThreadRoutine,
  (LPVOID)hwndChild, 0,(LPDWORD)&dwIDThread);

В качестве третьего параметра мы передаем этой функции адрес функции задачи ThreadRoutine, а в качестве четвертого - идентификатор дочернего MDI-окна, который необходим для выполенния рисования.

Заметим, что мы могли бы передать через четвертый параметр функции CreateThread XE "CreateThread" адрес структуры CHILD_WINDOW_TAG, дополнив эту структуру полем для хранения идентификатора дочернего MDI-окна. При этом функция задачи получила бы доступ ко всем необходимым ей параметрам. Однако в нашем приложении мы продемонстрировали оба способа, связанных с использованием параметра функции задачи и памяти окна.

После того как задача создана, ее идентификатор сохраняется в структуре CHILD_WINDOW_TAG:


lpTag->hThread = hThread;

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

На заключительном шаге в заголовке дочернего MDI-окна отображается системный номер задачи, который записывается функцией CreateThread XE "CreateThread" в переменную dwIDThread:


sprintf(szBuf, "Thread ID = %lX", dwIDThread);
SetWindowText(hwndChild, szBuf);

Напомним, что системный номер задачи и ее идентификатор - разные по смыслу и значению величины.

Обработка сообщений от меню Window

Меню Window предназначено для управления дочерними MDI-окнами. Выбирая строки этого меню, пользователь может упорядочить расположение окон одним из двух способов, упорядочить расположение пиктограмм минимизированных окон, а также закрыть все дочерние MDI-окна. Все эти операции выполняются посылкой соответствующих сообщений окну Client Window. Для экономии места мы не будем останавливаться на подробном описании соответствующих фрагментов кода. При необходимости обратитесь к 17 тому “Библиотеки системного программиста”.

Обработка сообщений от меню Priority

Меню Priority позволяет изменить класс приоритета процесса, в рамках которого работает приложение.

Процессам в нашей книге посвящена отдельная глава, однако изменение класса приоритета - достаточно простая операция, которая выполняется с помощью функции SetPriorityClass XE "SetPriorityClass" :


SetPriorityClass(GetCurrentProcess(), 
   REALTIME_PRIORITY_CLASS);

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

Через второй параметр функции SetPriorityClass XE "SetPriorityClass" передается новое значение класса приоритета.

Функция ChildWndProc

Функция ChildWndProc обрабатывает сообщения, поступающие в дочерние MDI-окна.

Обработка сообщения WM_PAINT

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

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

Для получения адреса структуры, который был записан в память дочернего окна, мы вызываем функцию GetWindowLong:


lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, GWL_USERDATA);

После этого выполняется вход в критическую секцию, получение контекста отображения, рисование во внутренней области дочернего MDI-окна строки Child Window, освобождение контекста отображения и выход из критической секции:


EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rc);
DrawText(hdc, "Child Window", -1, &rc,
  DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));
Обработка сообщения WM_CLOSE

Это сообщение поступает в функцию дочернего окна при уничтожении последнего (например, если пользователь сделал двойной щелчок левой клавишей мыши по пиктограмме системного меню дочернего MDI-окна).

Задача обработчика сообщения WM_CLOSE заключается в сбросе признака активности задачи, в результате чего задача завершает свою работу:


lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, 
  GWL_USERDATA);
lpMyWndTag->fActive = 0;  
Обработка сообщения WM_RBUTTONDOWN

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

Плавающее меню определено в ресурсах приложения с идентификатором IDR_POPUPMENU. Оно отображается на экране с помощью функции TrackPopupMenu. Соответствующая техника была нами описана в главе “Меню” 13 тома “Библиотеки системного программиста”.

Обработка сообщения WM_COMMAND

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

Если пользователь выбирает из этого меню, например, строку Suspend, выполняется приостановка работы задачи, запущенной для данного дочернего MDI-окна. Приостановка выполняется при помощи функции SuspendThread XE "SuspendThread". Идентификатор задачи, необходимый для нее, извлекается из поля hThread структуры типа CHILD_WINDOW_TAG:


lpMyWndTag = LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, 
  GWL_USERDATA);
EnterCriticalSection(&(lpMyWndTag->csChildWindowPaint));
SuspendThread(lpMyWndTag->hThread);
LeaveCriticalSection(&(lpMyWndTag->csChildWindowPaint));

Для возобновления выполнения приостановленной задачи мы использовали функцию ResumeThread XE "ResumeThread" :


ResumeThread(lpMyWndTag->hThread);

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

Относительный приоритет задачи изменяется фукнцией SetThreadPriority XE "SetThreadPriority", как это показано ниже:


SetThreadPriority(lpMyWndTag->hThread, 
  THREAD_PRIORITY_LOWEST);

Если выбрать из плавающего меню строку Get priority, с помощью функции GetThreadPriority XE "GetThreadPriority" определяется текущий относительный приоритет задачи, запущенной для данного дочернего MDI-окна. Значение этого приоритета отображается затем на экране при помощи простейшей диалоговой панели, создаваемой функцией MessageBox.

При выборе из плавающего меню строки Kill Thread задача будет принудительно уничтожена функцией TerminateThread XE "TerminateThread" :


TerminateThread(lpMyWndTag->hThread, 5);

В качестве кода завершения здесь передается произвольно выбранное нами значение 5.

С помощью плавающего меню вы можете удалить дочернее MDI-окно, завершив работу соответствующей задачи. Для этого окну Client Window посылается сообщение WM_MDIDESTROY XE "WM_MDIDESTROY" :


SendMessage(hwndClient, WM_MDIDESTROY, (WPARAM)hwnd, 0);

Все остальное делает обработчик сообщения WM_CLOSE, который получит управление при удалении дочернего MDI-окна. А именно, этот обработчик сбрасывает признак активности задачи, в результате чего задача завершает свою работу.

Функция задачи ThreadRoutine

Задача ThreadRoutine запускается для каждого вновь создаваемого дочернего MDI-окна. Ее функция получает один параметр - идентификатор этого дочернего окна, который необходим для выполнения рисования. Другие параметры, нужные для работы функции задачи ThreadRoutine, извлекаются из структуры типа CHILD_WINDOW_TAG. В свою очередь, адрес этой структуры извлекается из памяти окна перед началом цикла рисования:


lpMyWndTag = (LPCHILD_WINDOW_TAG)GetWindowLong(hwnd, 
  GWL_USERDATA);

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


if(!lpMyWndTag->fActive)
  break;

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

После прерывания цикла функция задачи удаляет ненужную более критическую секцию и освобождает память, заказанную для структуры CHILD_WINDOW_TAG функцией malloc XE "malloc" :


DeleteCriticalSection(&(lpMyWndTag->csChildWindowPaint));
free(lpMyWndTag);

Затем задача завершает свое выполнение с помощью оператора return.