Вы находитесь на странице: 1из 143

Министерство образования и науки Республики Казахстан

Таразский государственный университет им М.Х. Дулати

Г.С.Алтыбаев, А.Н.Иманбекова

СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ В ОС WINDOWS

Учебно-методическое пособие

Издательство «Тараз университеті»


Тараз, 2016
Алтыбаев Г.С., Иманбекова А.Н.
Системное программирование в ОС Windows: Учебно-методическое
пособие. – Тараз: Тараз университетi, 2016. – 143 стр.

Рецензенты:
Аязбаев Т.Л. – к.ф.-м.н., доцент кафедры «Математика и
вычислительная техника» Таразского инновационно-
гуманитарного университета;
Мамаев Ш.М. – д.ф.-м.н., заведующий кафедрой «Математика и
вычислительная техника» Таразского инновационно-
гуманитарного университета;
Сембина Г.К. – к.т.н., доцент, заведующая кафедрой
«Информационные системы» Таразского
государственного университета им. М.Х. Дулати.

В пособии рассмотрены вопросы использования интерфейса


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

© Алтыбаев Г.С., Иманбекова А.Н.


© ТарГУ им. М.Х. Дулати, 2016
Содержание

ВВЕДЕНИЕ 4
Лабораторная работа №1. Создание окон с помощью Win32 6
API функций
Лабораторная работа №2. Процессы и их создание в Win32 API 19
Лабораторная работа №3. Создание потоков в Win32 API 26
Лабораторная работа №4. Синхронизация потоков при помощи 32
семафоров и критических секций
Лабораторная работа №5. Синхронизация процессов при 52
помощи событий и мьютексов
Лабораторная работа №6. Обмен данными по анонимному 73
каналу с сервером
Лабораторная работа №7. Обмен данными по именованному 94
каналу с сервером
Лабораторная работа №8. Работа с файлами с помощью Win32 112
API функций
Лабораторная работа №9. Работа с атрибутами файлов с 120
помощью Win32 API функций
Лабораторная работа №10. Работа с каталогами с помощью 127
Win32 API функций
Лабораторная работа №11. Использование динамических 133
библиотек для создания приложений
Список рекомендованной литературы 143

3
ВВЕДЕНИЕ

Учебно-методическое пособие представляет собой лабораторный


практикум программирования в среде Microsoft Visual Studio 2010 с
использованием библиотеки Win32 API (далее Win32) – набора базовых
функций интерфейсов программирования приложений (API – Application
Programming Interface), работающих под управлением операционных систем
(ОС) семейства Windows.
Лабораторный практикум состоит из 11 лабораторных работ,
охватывающих основные разделы программирования в среде Win32. В нем
представлены примеры системного программирования приложений,
предназначенных для управления файловой системой, управления и
взаимодействия процессов и потоков, асинхронным вводом-выводом и т.д. В
каждой лабораторной работе приводится теоретический раздел, в котором
дается подробное описание используемых функций Win32, параметров
функций и их возможные значения, результат выполнения данной функции и
правила ее использования в программе. Поскольку программной средой
выполнения лабораторной работы является MS Visual Studio 2010, то в
лабораторных работах, которые посвящены созданию отдельных видов
приложений, описывается процесс создания проекта, приведены исходные
коды основных файлов проекта и технология компиляции файла программы.
Использование функций Win32 для выполнения лабораторной работы
приводится в программе, написанной на Visual Studio 2010.
Лабораторная работа состоит из следующих этапов:
1) домашняя подготовка;
2) выполнение работы на компьютере в соответствии с заданием;
3) сдача выполненной работы преподавателю на персональном
компьютере;
4) распечатка результатов работы на принтере;
5) оформление отчета;
5) защита лабораторной работы.
В процессе домашней подготовки студент изучает лекционный материал,
материалы по темам данного пособия и дополнительной литературы,
знакомится с заданием на выполнение лабораторной работы и готовит отчет по
выполнению лабораторной работы.
Выполнение лабораторной работы сводится к созданию исполняемого
модуля и запуску программы с соответствующими исходными данными во
время занятий в компьютерном классе кафедры «Информационные системы» в
присутствии преподавателя. В процессе выполнения лабораторной работы
студент последовательно выполняет задание, а по завершению работы –
демонстрирует преподавателю результаты.
После приема преподавателем лабораторной работы на ПК студент
сохраняет результаты лабораторной работы внешнем носителе, готовит отчет
по работе и распечатывает результаты на подготовленных листах формата А4.
Отчет по каждой лабораторной работе должен содержать:
− название работы;
4
− цель лабораторной работы;
− задание на выполнение лабораторной работы;
− алгоритмы программ;
После выполнения лабораторной работы производится защита, на которой
студенты должны пояснить технологию создания проекта задания, выбор
основных параметров проекта, использование функций Win32, процесс
создания Windows-приложения и ответить на контрольные вопросы,
приведенные в задании.
Использование данного пособия поможет студентам освоить процесс
использования функций Win32, программирование интерфейса и Windows
приложений под управлением ОС семейства Windows NT.

5
Лабораторная работа №1
Создание окон с помощью Win32 API функций

Цель работы:
1. Изучение принципов организации и функционирования стандартных
приложений Windows.
2. Изучение структуры приложений, использующих функции Win32API.
3. Создание каркаса оконного приложения Win32 и исследование его
структуры.

Краткое теоретическое введение


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

Головная функция
WinMain()
{
...
RegisterClass(&wc);

...
CreateWindow();

...
ShowWindow();
OS
... Windows Оконная
Цикл обработки сообщений функция
while();
{ WndProc()
{
} ...
return 0; }
}

Рисунок 1. Структура приложения Windows

6
Разработчиками операционной системы Windows была создана библиотека
функций, при помощи которых и происходит взаимодействие приложения с
операционной системой, так называемые функции Программного интерфейса
приложений (Application Program Interface, API).
Подмножество этих функций, предназначенных для графического вывода
на дисплей, графопостроитель и принтер, представляет собой Интерфейс
графических устройств (Graphics Device Interface, GDI).
Библиотека API-функций разрабатывалась в расчете на то, что ее можно
использовать для любого языка программирования, а поскольку разные языки
имеют различные типы данных, то были созданы собственные Windows-типы,
которые приводятся к типам данных языков программирования. Отметим
только, что в Windows нет логического типа bool, но есть Windows-тип BOOL,
который эквивалентен целому типу int. Будем рассматривать типы данных
Windows по мере необходимости.
Еще одной особенностью API-функций является использование обратного,
по отношению к принятому в языке С, порядка передачи параметров, как это
реализовано в языке Pascal. В С для идентификации таких функций
использовалось служебное слово pascal, в Windows введены его синонимы
CALLBACK, APIENTRY или WINAPI. По умолчанию С-функции передают
параметры, начиная с конца списка так, что первый параметр всегда находится
на вершине стека. Именно это позволяет использовать в языке С функции с
переменным числом параметров, что в API-функциях невозможно.

Каркас Windows-приложения
В отличие от программы, выполняемой в операционной системе MS-DOS,
даже для создания простейшего приложения под Windows придется проделать
намного больше работы. Чтобы иметь возможность работать с оконным
интерфейсом, заготовка или каркас Windows-приложения должна выполнить
некоторые стандартные действия:
1. Определить класс окна.
2. Зарегистрировать окно.
3. Создать окно данного класса.
4. Отобразить окно.
5. Запустить цикл обработки сообщений.
Термин интерфейс здесь следует понимать как способ взаимодействия
пользователя и приложения. Класс окна – структура, определяющая его
свойства.
С помощью листинга 1 рассмотрим "каркас" Windows-приложения.

Листинг 1. Минимальный код каркаса Windows-приложения


#include <windows.h>
#include <tchar.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
TCHAR WinName[] = _T("MainFrame");
int APIENTRY _tWinMain(HINSTANCE This, // Дескриптор текущего
приложения
HINSTANCE Prev, // В современных системах всегда 0
7
LPTSTR cmd, // Командная строка
int mode) // Режим отображения окна
{
HWND hWnd; // Дескриптор главного окна программы
MSG msg; // Структура для хранения сообщения
WNDCLASS wc; // Класс окна
// Определение класса окна
wc.hInstance = This;
wc.lpszClassName = WinName; // Имя класса окна
wc.lpfnWndProc = WndProc; // Функция окна
wc.style = CS_HREDRAW | CS_VREDRAW; // Стиль окна
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // Стандартная
иконка
wc.hCursor = LoadCursor(NULL,IDC_ARROW); // Стандартный
курсор
wc.lpszMenuName = NULL; // Нет меню
wc.cbClsExtra = 0; // Нет дополнительных данных класса
wc.cbWndExtra = 0; // Нет дополнительных данных окна
// Заполнение окна белым цветом
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
if(!RegisterClass(&wc)) return 0; // Регистрация класса
окна
// Создание окна
hWnd = CreateWindow(WinName, // Имя класса окна
_T("Каркас Windows-приложения"), // Заголовок окна
WS_OVERLAPPEDWINDOW, // Стиль окна
CW_USEDEFAULT, // x
CW_USEDEFAULT, // y Размеры окна
CW_USEDEFAULT, // Width
CW_USEDEFAULT, // Height
HWND_DESKTOP, // Дескриптор родительского окна
NULL, // Нет меню
This, // Дескриптор приложения
NULL); // Дополнительной информации нет
ShowWindow(hWnd, mode); //Показать окно
// Цикл обработки сообщений
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);// Функция трансляции кодов нажатой
клавиши
DispatchMessage(&msg); // Посылает сообщение функции WndProc()
}
return 0;
}
// Оконная функция вызывается операционной системой
// и получает сообщения из очереди для данного приложения
LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{ // Обработчик сообщений
switch(message)
{
case WM_DESTROY : PostQuitMessage(0);
break; // Завершение программы
// Обработка сообщения по умолчанию
default : return DefWindowProc(hWnd, message, wParam, lParam);

8
}
return 0;
}

Исследование каркаса Windows-приложения


Рассмотрим подробно текст программы. Первая строка содержит файл
включений, который обязательно присутствует во всех Windows-программах.
#include <windows.h>
Если в ранних версиях Visual Studio этот файл содержал основные
определения, то сейчас он служит для вызова других файлов включений,
основные из которых: windef.h, winbase.h, wingdi.h, winuser.h; а также несколько
дополнительных файлов, в которых помещены определения API-функций,
констант и макросов.
Дополнительно подключим:
#include <tchar.h>
В этом файле содержатся определения некоторых полезных макросов,
например, макрос _Т() служит для создания строки Unicode на этапе
компиляции и определен примерно так:
#define _T(x) T(x)
#ifdef _UNICODE
#define __T(x) L ## x
#else
#define __T(x) x
#endif

Рисунок 2. Страница общих свойств проекта

Макрос преобразуется в оператор "L", который является инструкцией


компилятору для образования строки UNICODE, если определена константа
UNICODE; и в "пустой оператор", если константа не определена. Константа
UNICODE устанавливается в зависимости от установок свойства проекта
Character Set (рисунок 2). Диалоговое окно свойств Property Pages доступно
сейчас на подложке Property Manager панели управления Solution Explorer.
Таким образом, этот макрос позволяет компилировать проект как в
кодировке Unicode, так и в Windows-кодировке. Мы подробно рассмотрели

9
данный макрос потому, что многие определения Windows описаны подобным
образом.
Далее следует прототип оконной функции:
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
Оконная функция также является функцией обратного вызова, что связано
с некоторыми особенностями организации вызовов операционной системы. Эта
функция регистрируется в системе, а ее вызов осуществляет операционная
система, когда требуется обработать сообщение. Тип возвращаемого значения
функции LRESULT эквивалентен long для Win32-проекта.
На глобальном уровне описывается имя класса окна приложения в виде
текстовой строки:
TCHAR WinName [] = _T("MainFrame");
Тип TCHAR также преобразуется в wchar_t, если определена константа
_UNICODE, и в char, если константа не определена.
Тип wchar_t эквивалентен типу short и служит для хранения строк в
кодировке Unicode, где для одного символа выделяется 16 бит.
Имя класса окна используется операционной системой для его
идентификации. Имя может быть произвольным, в частности содержать
кириллический текст.
Рассмотрим заголовок головной функции:
int APIENTRY _tWinMain(HINSTANCE This, // Дескриптор текущего
приложения
HINSTANCE Prev, // В современных системах всегда 0
LPTSTR cmd, // Командная строка
int mode) // Режим отображения окна
Для Windows-приложений с Unicode она носит имя wWinMain(), а в 8-
битной кодировке -WinMain(), выбор варианта определяется префиксом _t, что
также является стандартным приемом в библиотеке API-функций. Функция
имеет четыре параметра, устанавливаемых при загрузке приложения:
− This – дескриптор, присваиваемый операционной системой при загрузке
приложения;
− Prev – параметр предназначен для хранения дескриптора предыдущего
экземпляра приложения, уже загруженного системой. Сейчас он
потерял свою актуальность и сохранен лишь для совместимости со
старыми приложениями (начиная с Windows 95, параметр
устанавливается в нулевое значение);
− cmd – указатель командной строки, но без имени запускаемой
программы. Тип LPTSTR эквивалентен TCHAR*;
− mode – режим отображения окна.
Дескриптор (описатель) – тип данных Windows, который используется для
описания объектов операционной системы. Дескриптор напоминает индекс
хеш-таблицы и позволяет отслеживать состояние объекта в памяти при его
перемещении по инициативе операционной системы. Предусмотрено много
типов дескрипторов: HINSTANCE, HWND и др., но все они являются 32-
разрядными целыми числами.
Внутри головной функции описаны три переменные:

10
− hWnd — предназначена для хранения дескриптора главного окна
программы;
− msg — это структура, в которой хранится информация о сообщении,
передаваемом операционной системой окну приложения:
struct MSG
{
HWND hWnd; // Дескриптор окна
UINT message; // Номер сообщения
WPARAM wParam; // 32-разрядные целые содержат
LPARAM lParam; // дополнительные параметры сообщения
DWORD time; // Время посылки сообщения в миллисекундах
POINT pt; // Координаты курсора (x,y)
};
struct POINT
{
LONG x,y;
};
Тип WPARAM – "короткий параметр" был предназначен для передачи 16-
разрядного значения в 16-разрядной операционной системе, в Win32 это такое
же 32-разрядное значение, что и LPARAM.
– wc – структура, содержащая информацию по настройке окна.
Требуется заполнить следующие поля:
wc.hlnstance = This; Дескриптор текущего приложения.
wc.lpszClassName = WinName;
Имя класса окна.
• wc.lpfnWndProc = WndProc;
Имя оконной функции для обработки сообщений.
• wc.style = CS_HREDRAW | CS_VREDRAW;
Такой стиль определяет автоматическую перерисовку окна при изменении
его ширины или высоты.
• wc.hIcon = LoadIcon(NULL,IDI_APPLICATION);
Дескриптор пиктограммы (иконки) приложения. Функция LoadIcon()
обеспечивает ее загрузку. Если первый параметр NULL, используется
системная пиктограмма, которая выбирается по второму параметру из
следующего набора:
– IDI_APPLICATION — стандартная иконка;
– IDI_ASTERISK — звездочка;
– IDI_EXCLAMATION — восклицательный знак;
– IDI_HAND — ладонь;
– IDI_QUESTION — вопросительный знак;
– IDI_WINLOGO — логотип Windows;
• wc.hCursor = LoadCursor(NULL,IDC_ARROW);
Аналогичная функция LoadCursor () обеспечивает загрузку графического
образа курсора, где нулевой первый параметр также означает использование
системного курсора, вид которого можно выбрать из списка:
– IDC_ARROW — стандартный курсор;
– IDC_APPSTARTING — стандартный курсор и маленькие песочные часы;
– IDC_CROSS — перекрестие;
– IDC_IBEAM — текстовый курсор;
11
– IDC_NO — перечеркнутый круг;
– IDC_SIZEALL — четырехлепестковая стрелка;
– IDC_SIZENESW — двухлепестковая стрелка, северо-восток и юго-запад;
– IDC_SIZENWSE — двухлепестковая стрелка, северо-запад и юго-восток;
– IDC_SIZENS — двухлепестковая стрелка, север и юг;
– IDC_SIZEWE — двухлепестковая стрелка, запад и восток;
– IDC_UPARROW — стрелка вверх;
– IDC_WAIT — песочные часы;
• wc.lpszMenuName = NULL;
Ссылка на строку главного меню, при его отсутствии NULL.
• wc.cbClsExtra = 0;
Дополнительные параметры класса окна.
• wc.cbWndExtra = 0; Дополнительные параметры окна.
• wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
Дескриптор кисти, которая используется для заполнения окна.
Стандартная конструкция, создает системную кисть белого цвета
WHITE_BRUSH. Требуется явное преобразование типа — HBRUSH.
После того как определены основные характеристики окна, можно это
окно создать при помощи API-функции CreateWindow(), где также нужно задать
параметры:
1. WinName — имя, которое присвоено классу окна.
2. _T("Каркас Windows-приложения") — заголовок окна в виде строки
Unicode либо С-строки.
3. WS_OVERLAPPEDWINDOW — макрос, определяющий стиль
отображения стандартно-
го окна, имеющего системное меню, заголовок, рамку для изменения размеров,
а также кнопки минимизации, развертывания и закрытия. Это наиболее общий
стиль окна, он определен так:
#define WS_OVERLAPPEDWINDOW
(WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|
WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX)
Можно создать другой стиль, используя комбинацию стилевых макросов
при помощи операции логического сложения, вот некоторые из них:
WS_OVERLAPPED — стандартное окно с рамкой;
WS_CAPTION — окно с заголовком;
WS_THICKFRAME — окно с рамкой;
WS_MAXIMIZEBOX — кнопка распахивания окна;
WS_MINIMIZEBOX — кнопка минимизации;
WS_SYSMENU — системное меню;
WS_HSCROLL — горизонтальная панель прокрутки;
WS_VSCROLL — вертикальная панель прокрутки;
WS_VISIBLE — окно отображается;
WS_CHILD — дочернее окно;
WS_POPUP — всплывающее окно;
4. Следующие два параметра определяют координаты левого верхнего угла
окна (x,y) , еще два параметра: Width — ширину и Height — высоту окна в

12
пикселах. Задание параметра CW_USEDEFAULT означает, что система
сама выберет для отображения окна наиболее (с ее точки зрения) удобное
место и размер.
5. Следующий параметр — указатель на структуру меню, или NULL, при
его отсутствии.
6. Далее требуется указать дескриптор приложения, владельца окна — This.
7. И, наконец, указатель на дополнительную информацию, в нашем случае
– NULL.
Окно создано, и с ним можно работать, но пока оно не отображается. Для
того чтобы окно увидеть, необходимо его отобразить с помощью функции
ShowWindow(hWnd, mode), которая принимает два параметра: hWnd –
дескриптор окна и mode – режим отображения. В нашем случае используется
значение, полученное при открытии приложения через параметр головной
функции.
Далее, заключительная часть головной функции – цикл обработки
сообщений. Он задается оператором while, аргументом которого является
функция GetMessage(&msg, NULL, 0, 0). Такой цикл является обязательным для
всех Windows-приложений, его цель — получение и обработка сообщений,
передаваемых операционной системой. Операционная система ставит
сообщения в очередь, откуда они извлекаются функцией GetMessage() по мере
готовности приложения:
□ первым параметром функции является &msg — указатель на структуру
MSG, где и хранятся сообщения;
□ второй параметр hWnd — определяет окно, для которого предназначено
сообщение, если же необходимо перехватить сообщения всех окон данного
приложения, он должен быть NULL;
□ остальные два параметра определяют [min, max] диапазон получаемых
сообщений. Чаще всего необходимо обработать все сообщения, тогда эти
параметры должны быть равны 0.
Сообщения определяются их номерами, символические имена для них
определены в файле включений winuser.h. Префикс всех системных сообщений
WM_.
Внутри цикла расположены две функции:
TranslateMessage(&msg);
DispatchMessage(&msg);
Первая из них транслирует код нажатой клавиши в клавиатурные
сообщения WM_CHAR. При этом в переменную wParam структуры msg
помещается код нажатой клавиши в Windows-кодировке CP-1251, в младшее
слово lParam — количество повторений этого сообщения в результате
удержания клавиши в нажатом состоянии, а в старшее слово — битовая карта
со значениями, приведенными в таблица 1.
Использование этой функции не обязательно и нужно только для
обработки сообщений от клавиатуры.
Вторая функция, DispatchMessage(&msg), обеспечивает возврат
преобразованного сообщения обратно операционной системе и инициирует
вызов оконной функции данного приложения для его обработки.
13
Данным циклом и заканчивается головная функция.
Далее следует описать оконную функцию WndProc(), и построение каркаса
Windows-приложения будет закончено.

Таблица 1. Битовая карта клавиатуры, HIWORD(lParam)


Бит Значение
15 1, если клавиша отпущена, 0 — если нажата
14 1, если клавиша была нажата перед посылкой сообщения
13 1, если нажата клавиша <Alt>
12–9 Резерв
8 1, если нажата функциональная клавиша
7–0 Scan-код клавиши

Основной компонент этой функции – переключатель switch,


обеспечивающий выбор соответствующего обработчика сообщений по его
номеру message. В нашем случае предусмотрена обработка лишь одного
сообщения WM_DESTROY. Это сообщение посылается, когда пользователь
завершает программу. Получив его, оконная функция вызывает функцию
PostQuitMessage(0), которая завершает приложение и передает операционной
системе код возврата – 0. Точнее генерируется сообщение WM_QUIT, получив
которое функция GetMessage () возвращает нулевое значение. В результате цикл
обработки сообщений прекращается и происходит завершение работы
приложения.
Все остальные сообщения обрабатываются по умолчанию функцией
DefWindowProc(), имеющей такой же список параметров и аналогичное
возвращаемое значение, поэтому ее вызов помещается после оператора return.

Листинг 2. Простое окно сообщений


#include <windows.h> // заголовочный файл, содержащий функции
API

// Основная функция - аналог int main() в консольном


приложении:
int WINAPI WinMain(HINSTANCE hInstance, // дескриптор
экземпляра приложения
HINSTANCE hPrevInstance, // в Win32 не используется
LPSTR lpCmdLine, // нужен для запуска окна в режиме командной
строки
int nCmdShow) // режим отображения окна
{
// Функция вывода окна с кнопкой "ОК" на экран (о параметрах
позже)
MessageBox(NULL, L"Привет, мир!!!", L"Оконная процедура",
MB_OK);
return NULL; // возвращаем значение функции
}
Листинг 3. Сложное окно сообщений
#include <windows.h> // содержит API
// Основная функция:
14
int WINAPI WinMain (HINSTANCE hInst, // дескриптор экземпляра
приложения
HINSTANCE hPreviousInst, // в Win32 не используется, но
объявление нужно
LPSTR lpCommandLine, // нужен для запуска окошка в режиме
командной строки
int nCommandShow) // режим отображения окна
{
int result = MessageBox(NULL, L"Вам нравится WINAPI?!",
L"Задача",
MB_ICONQUESTION | MB_YESNO);
switch (result)
{
case IDYES: MessageBox (NULL, L"Продолжайте в том же духе!!!",
L"Ответ", MB_OK| MB_ICONASTERISK); break;
case IDNO: MessageBox (NULL, L"Очень жаль!!!", L"Ответ",
MB_OK| MB_ICONSTOP); break;
}
return NULL;
}

Третьим параметром могут быть записаны следующие идентификаторы:


параметры кнопок:
MB_ABORTRETRYIGNORE — три кнопки: ABORT, RETRY, IGNORE
MB_CANCELTRYCONTINUE — три кнопки: CANCEL, TRY,
CONTINUE
MB_HELP MB_OK MB_OKCANCEL — 2 кнопки: OK, CANCEL
MB_RETRYCANCEL — 2 кнопки: RETRY, CANCEL
MB_YESNO — 2 кнопки: YES, NO
MB_YESNOCANCEL — три кнопки: YES, NO, CANCEL
параметры пиктограммы:
MB_ICONSTOP — выводит (крестик)
MB_ICONQUESTION — выводит (знак вопроса)
MB_ICONEXCLAMATION — выводит восклицательный знак в
треугольнике
MB_ICONASTERISK — выводит (восклицательный знак)
А возвращать значения при нажатии вышеуказанных кнопок функция
MessageBox будет такие:
IDABORT — при нажатии на ABORT
IDCANCEL – …….на кнопку CANSEL
IDCONTINUE – ……..на кнопку CONTINUE
IDIGNORE – …….на кнопку IGNORE
IDNO – …….на кнопку NO
IDOK – …….на кнопку OK
IDRETRY – …….на кнопку RETRY
IDTRYAGAIN – …….на кнопку TRY AGAIN
IDYES – …….на кнопку YES

15
Порядок выполнения работы
Рассмотрим сначала, как можно "вручную" создать минимальное
приложение Win32. Загрузив Visual Studio 2010, выполним команду File | New |
Project... и выберем тип проекта — Win32 Project. В раскрывающемся списке
Location выберем путь к рабочей папке, а в поле Name имя проекта (рисунок
3). В следующем диалоговом окне, приведенном на рисунке 4, нажимаем
кнопку Next, а в окне опций проекта (рисунок 5) выберем флажок Empty
project (Пустой проект) и нажмем кнопку Finish — получим пустой проект, в
котором нет ни одного файла.

Рисунок 3. Выбор типа проекта

Рисунок 4. Стартовое окно построителя приложения

16
Рисунок 5. Окно опций проекта

С помощью контекстного меню (рисунок 6) добавим файл для кода


приложения, имя файла введем в ходе диалога выбора шаблона объекта на
рисунке 7. (Тот же самый диалог можно получить по команде меню Project |
Add New Item)

Рисунок 6. Добавление к проекту нового объекта с помощью контекстного


меню

Рисунок 7. Выбор шаблона объекта

17
Программа не делает ничего полезного, поэтому, запустив ее на
выполнение кнопкой ► (Start Debugging), мы получим изображенное на
рисунке 8 пустое окно, имеющее заголовок и набор стандартных кнопок.

Рисунок 8. Окно первой Windows-программы

Варианты заданий на выполнение


1. После нажатия на левую (правую) клавишу мыши над рабочей областью
окна в левом верхнем (правом нижнем) углу области отобразить
временное окно размером в четверть области. Временное окно скрыть
после отжатия клавиши в любом месте экрана.
2. В рабочей области окна приложения рядом друг с другом расположить 4
временных окна, в заголовках которых указан номер окна. После нажатия
левой клавиши мыши временное окно выдает сообщение, содержащее
номер окна.
3. Окно размером в четверть площади экрана расположено в центре экрана.
После нажатия левой клавиши мыши окно несколько раз меняет
подсветку и перемещается в угол экрана так, что курсор мыши оказыва-
ется за пределами окна.
4. Дочернее окно размером 100*100 пикселей при перемещении курсора
мыши над ним "убегает" от курсора мыши в произвольном направлении,
оставаясь в пределах рабочей области родительского окна.
5. В центре рабочей области окна расположено окно без заголовка с
вертикальной и горизонтальной полосами просмотра размером в четверть
рабочей области. При нажатии разных клавиш мыши временное окно
выдает разный звуковой сигнал

Контрольные вопросы
1. Что такое Win32 API?
2. Какие операционные системы обслуживает API Win32?
3.Какие особенности имеет Win32 API?
4. Какие преимущества программирования дает Win32 API?
5. Какой основной тип переменных используется в Win32?
6. Для управления каких систем могут быть написаны программы с
использованием Win32?

18
Лабораторная работа №2
Тема: Процессы и их создание в Win32 API

Цель работы:
1. Изучение основных функций Win32API, используемых для управления
процессами
2. Разработка простейшей программы, демонстрирующей создание и
завершение процесса.
3. Разработка приложения Win32 API, реализующего функции указанные в
варианте.

Краткое теоретическое введение


1. Процессы в Windows
1.1. Создание процесса
В Windows под процессом понимается объект ядра, которому принадлежат
системные ресурсы, используемые приложением. Поэтому можно сказать, что в
Windows процессом является приложение. Выполнение каждого процесса
начинается с первичного потока. В процессе своего исполнения процесс может
создавать другие потоки. Исполнение процесса заканчивается при завершении
работы всех его потоков. Процесс может быть также завершен вызовом
функций ExitProcess и TerminateProcess, которые будут рассмотрены в
следующем параграфе.
Новый процесс в Windows создается вызовом функции CreateProcess,
которая имеет следующий прототип:
BOOL CreateProcess(
LPCTSTR lpApplicationName,// имя исполняемого модуля
LPTSTR lpCommandLine, // командная строка
LPSECURITY_ATTRIBUTES lpProcessAttributes,//атрибуты защиты
процесса
LPSECURITY_ATTRIBUTES lpThreadAttributes,//атрибуты защиты
потока
BOOL bInheritHandle, // наследуемый ли дескриптор
DWORD dwCreationFlags, // флаги создания процесса
LPVOID lpEnvironment, // блок новой среды окружения
LPCTSTR lpCurrentDirectory, // текущий каталог
LPSTARTUPINFO lpStartUpInfo, // вид главного окна
LPPROCES S_INFORMATION lpProcessInformation // информация о
процессе
);
Функция CreateProcess возвращает значение TRUE, если процесс был
создан успешно. В противном случае эта функция возвращает значение FALSE.
Процесс, который создает новый процесс, называется родительским процессом
(parent process) по отношению к создаваемому процессу. Новый же процесс,
который создается другим процессом, называется дочерним процессом (child
process) по отношению к процессу родителю.
Сейчас мы опишем только назначение некоторых параметров функции
CreateProcess. Остальные параметры этой функции будут описываться по мере
19
их использования. Первый параметр lpApplicationName определяет строку с
именем exe-файла, который будет запускаться при создании нового процесса.
Эта строка должна заканчиваться нулем и содержать полный путь к
запускаемому файлу. Для примера рассмотрим следующую программу, которая
выводит на консоль свое имя и параметры.

Листинг 1. Консольный процесс, который выводит на консоль свое имя и


параметры
#include <conio.h>
int main(int argc, char *argv[])
{
int i;
_cputs("I am created.");
_cputs("\nMy name is: ");
_cputs(argv[0]);
for (i = 1; i < argc; i++)
_cprintf ("\n My %d parameter = %s", i, argv[i]);
_cputs("\nPress any key to finish.\n");
_getch();
return 0;
}
Создадим из этой программы exe-файл, который расположим на диске C и
назовем ConsoleProcess.exe. Тогда этот exe-файл может быть запущен из
другого приложения следующим образом.

Листинг 2. Пример консольного процесса, который создает другое


консольное приложение с новой консолью и ждет завершения работы этого
приложения.
#include <windows.h>
#include <conio.h>
int main()
{
char lpszAppName[] = "C:\\ConsoleProcess.exe";
STARTUPINFO si;
PROCESS_INFORMATION piApp;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
// создаем новый консольный процесс
if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &piApp))
{
_cputs("The new process is not created.\n");
_cputs("Check a name of the process.\n");
_cputs("Press any key to finish. \n");
_getch(); return 0;
}
_cputs("The new process is created.\n");
//ждем завершения созданного процесса
WaitForSingleObject(piApp.hProcess, INFINITE);
// закрываем дескрипторы этого процесса в текущем процессе
CloseHandle(piApp.hThread);
CloseHandle(piApp.hProcess);
20
return 0;
}

Отметим в последней программе два момента. Во-первых, перед запуском


консольного процесса ConsoleProcess.exe все поля структуры si типа
STARTUPINFO должны заполняться нулями. Это делается при помощи вызова
функции ZeroMemory, которая предназначена для этой цели и имеет
следующий прототип:
VOID ZeroMemory(
PVOID Destination, // адрес блока памяти
SIZE_T Length // длина блока памяти
);
В этом случае вид главного окна запускаемого приложения определяется
по умолчанию самой операционной системой Windows. Во-вторых, в параметре
dwCreationFlags устанавливается флаг CREATE_NEW_CONSOLE. Это говорит
системе о том, что для нового создаваемого процесса должна быть создана
новая консоль. Если этот параметр будет равен NULL, то новая консоль для
запускаемого процесса не создается и весь консольный вывод нового процесса
будет направляться в консоль родительского процесса.
Структура piApp типа PROCESS_INFORMATION содержит
идентификаторы и дескрипторы нового создаваемого процесса и его главного
потока. Мы не используем эти дескрипторы в нашей программе и поэтому
закрываем их. Значение FALSE параметра bInheritHandle говорит о том, что эти
дескрипторы не являются наследуемыми. О наследовании дескрипторов мы
поговорим подробнее в одном из следующих параграфов этой главы.
Теперь запустим наш новый консольный процесс другим способом,
используя второй параметр функции CreateProcess. Это можно сделать при
помощи следующей программы.

Листинг 3. Пример процесса, который создает новое консольное


приложение с новой консолью
#include <windows.h>
#include <conio.h>
int main()
{
char lpszCommandLine[] = "C:\\01-1-ConsoleProcess.exe p1 p2
p3";
STARTUPINFO si;
PROCESS_INFORMATION piCom;
ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb =
sizeof(STARTUPINFO);
// создаем новый консольный процесс
CreateProcess(NULL, lpszCommandLine, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &piCom);
// закрываем дескрипторы этого процесса CloseHandle(piCom.
hThread);
CloseHandle(piCom.hProcess);
_cputs("The new process is created.\n");
_cputs("Press any key to finish.\n");
_getch(); return 0;
21
}
Отличие этой программы от предыдущей состоит в том, что мы передаем
системе имя нового процесса и его параметры через командную строку. В этом
случае имя нового процесса может и не содержать полный путь к exe-файлу, а
только имя самого exe-файла. При использовании параметра lpCommandLine
система для запуска нового процесса осуществляет поиск требуемого exe-файла
в следующей последовательности каталогов:
– каталог из которого запущено приложение;
– текущий каталог родительского процесса;
– системный каталог Windows;
– каталог Windows;
– каталоги, которые перечислены в переменной PATH среды окружения.
Для иллюстрации сказанного запустим приложение Notepad.exe, используя
командную строку. Программа, запускающая блокнот из командной строки,
выглядит следующим образом.

Листинг 4. Пример запуска процесса Notepad


#include <windows.h>
#include <iostream> using namespace std;
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
// заполняем значения структуры STARTUPINFO по умолчанию
ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO);
// запускаем процесс Notepad if (!CreateProcess(
NULL, // имя не задаем
"Notepad.exe", // командная строка, первая лексема указывает
имя программы NULL, // атрибуты защиты процесса устанавливаем по
умолчанию
NULL, // атрибуты защиты первичного потока по умолчанию
FALSE, // дескрипторы текущего процесса не наследуются
новым процессом
0, // по умолчанию NORMAL_PRIORITY_CLASS
NULL, // используем среду окружения вызывающего процесса
NULL, // текущий диск и каталог, как и в вызывающем процессе
&si, // вид главного окна - по умолчанию
&pi // здесь будут дескрипторы и идентификаторы
// нового процесса и его первичного потока
)
)
{
cout << "The mew process is not created." << endl << "Check a
name of the process." << endl;
return 0;
}
4
Sleep(1000); // немного подождем и закончим свою работу
// закроем дескрипторы запущенного процесса в текущем процессе
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);

22
return 0;
}

1.2. Завершение процессов


Процесс может завершить свою работу вызовом функции ExitProcess,
которая имеет следующий прототип:
VOID ExitProcess(
UINT uExitCode // код возврата для всех потоков
);
При вызове функции ExitProcess завершаются все потоки процесса с кодом
возврата, который является параметром этой функции. Приведем пример
программы, которая завершает свою работу вызовом функции ExitProcess.

Листинг 5. Пример завершения процесса функцией ExitProcess


#include <windows.h>
#include <iostream> using namespace std;
volatile UINT count; volatile char c;
void thread()
{
for ( ; ; )
{
count++;
Sleep(100);
}
}
int main()
{
HANDLE hThread;
DWORD IDThread;
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread,
NULL, 0, &IDThread); if (hThread == NULL)
return GetLastError();
for ( ; ; )
{
cout << "Input 'y' to display the count or 'e' to exit: "; cin
>> (char)c; if (c == 'y')
cout << "count = " << count << endl; if (c == 'e')
ExitProcess(l);
}
}
Один процесс может завершить другой процесс при помощи вызова
функции TerminateProcess, которая имеет следующий прототитп:
BOOL TerminateProcess(
HANDLE hProcess,
UINT uExitCode
);
Если функция TerminateProcess выполнилась успешно, то она возвращает
значение равно TRUE. В противном случае возвращаемое значение равно
FALSE. Функция TerminateProcess завершает работу процесса, но не
освобождает все ресурсы, принадлежащие этому процессу. Поэтому эта
функция должна вызываться только в аварийных ситуациях при зависании

23
процесса.
Приведем программу, которая демонстрируют работу функции
TerminateProcess. Для этого сначала создадим бесконечный процесс-счетчик,
который назовем ConsoleProcess.exe и расположим на диске C.

Листинг 6. Пример бесконечного процесса


#include <windows.h>
#include <iostream> using namespace std; int count;
void main()
{
for ( ; ; )
{
count++;
Sleep(1000);
cout << "count = " << count << endl;
}
}
Ниже приведена программа, которая создает этот процесс, а потом
завершает его по требованию пользователя.

Листинг 7. Пример процесса, который создает другое консольное


приложение с новой консолью, а потом завершает его при помощи функции
TerminateProcess
#include <windows.h>
#include <conio.h>
int main()
{
char lpszAppName[] = "C:\\ConsoleProcess.exe";
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb=sizeof(STARTUPINFO);
// создаем новый консольный процесс
if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
{
_cputs("The new process is not created.\n");
_cputs("Check a name of the process.\n");
_cputs("Press any key to finish. \n");
_getch(); return 0;
}
_cputs("The new process is created.\n");
while (true)
{
char c;
_cputs("Input 't' to terminate the new console process: ");
c = _getch(); if (c == 't')
{
_cputs("t\n");
// завершаем новый процесс
TerminateProcess(pi.hProcess, 1)
; break;

24
}
}
// закрываем дескрипторы нового процесса в текущем процессе
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
Заданиe на выполнение
Написать программы двух консольных процессов Parent и Child, которые
выполняют следующие действия.
Процесс Parent:
1. Создает бинарный файл, записи которого имеют следующую структуру:
struct emp
{
int num; // номер зачетки
char name[10]; // имя студента
double grade; // средний бал
};
Имя файла и данные о студентах вводятся с консоли.
2. Выводит созданный файл на консоль.
3. Запрашивает с консоли номер зачетки, имя студента и новый средний
бал этого студента.
4. Формирует командную строку, которая содержит имя созданного файла
и информацию, полученную в пункте 3.
5. Запускает дочерний процесс Child, которому как параметр передается
командная строка, сформированная в пункте 4.
6. Ждет завершения работы процесса Child.
7. Выводит откорректированный файл на консоль.
8. Завершает свою работу.
Процесс Child:
1. Выводит на консоль информацию, полученную через командную строку.
2. Корректирует в файле, созданном процессом Parent, нужную запись, т.е.
устанавливает новый средний бал студента.
3. Завершает свою работу.
Для ожидания завершения работы процесса Child использовать
функцию:
DWORD WaitForSingleObject(
HANDLE hHandle, // дескриптор объекта
DWORD dwMilliseconds // интервал ожидания в миллисекундах
);
где второй параметр установить равным INFINITE, например
WaitForSingleObject(hProcess, INFINITE); // ждать завершения
процесса
Здесь hProcess – дескриптор процесса Child.

Контрольные вопросы
1. С помощью каких функций можно создать процесс?
2. С помощью каких функций можно удалит процесс?
3. Какую фунцию выполняет CreateProcess?
25
4. Какую фунцию выполняет OpenProcess?
5. Какую фунцию выполняет ExitProcess ?
6. Какую фунцию выполняет WaitForSingleObject?

Лабораторная работа №3
Тема: Создание потоков в Win32 API

Цель работы:
1. Изучение основных функций Win32 API, используемых для управления
потоками.
2. Разработка простейшей программы, демонстрирующей создание и
завершение процесса.
3. Разработка приложения Win32 API, реализующую указанные в варианте
функции.

Краткое теоретическое введение


1. Обзор потоков
Потоки позволяют в рамках одной программы решать несколько задач
одновременно. С недавних пор операционные системы для персональных
компьютеров сделали это возможным. Пользователи действительно могут
запускать одновременно более одной задачи. Планируя время центрального
процессора ОС распределяют его между потоками, а не между приложениями.
Чтобы использовать все преимущества, обеспечиваемые несколькими
процессорами в современных операционных системах, программист должен
знать, как создавать потоки.
В данной лабораторной работе рассматриваются следующие вопросы:
− что такое потоки;
− разница между потоком и процессом;
− преимущества потоков;
− функции Win32 для работы с потоками;
− реализация многопоточного приложения;
Определение потока довольно простое: потоки – это объекты, получающие
время процессора. Время процессора выделяется квантами (quantum, time slice).
Квант времени — это интервал, имеющийся в распоряжении потока до тех пор.
пока время не будет передано в распоряжение другого потока.
Обратите внимание, что кванты выделяются не программам или
процессам, а порожденным ими потокам. Как минимум, каждый процесс имеет
хотя бы один (главный) поток, но современные операционные системы
позволяют запустить в рамках процесса несколько потоков.
Если задачи приложения можно разделить на различные подмножества:
обработка событий, ввод/вывод, связь и др., то потоки могут быть органично
встроены в программное решение. Если разработчик может разделить большую
задачу на несколько мелких, это только повысит переносимость кода и
возможности его многократного использования.
Сделав приложение многопоточным, программист получает
дополнительные возможности управления им. Например, через управление
26
приоритетами потоков. Если один из них "притормаживает" приложение,
занимая слишком много процессорного времени, его приоритет может быть
понижен.
Другое важное преимущество внедрения потоков — при возрастании
"нагрузки" на приложение можно увеличить количество потоков и тем самым
снять проблему.
Потоки упрощают жизнь тем программистам, которые разрабатывают
приложения в архитектуре клиент/сервер. Когда требуется обслуживание
нового клиента, сервер может запустить специально для этого отдельный
поток. Такие потоки принято называть симметричными потоками (symmetric
threads) – они имеют одинаковое предназначение, исполняют один и тот же код
и могут разделять одни и те же ресурсы. Более того, приложения, рассчитанные
на серьезную нагрузку, могут поддерживать пул (pool) однотипных потоков.
Поскольку создание потока требует определенного времени, для ускорения
работы желательно заранее иметь нужное число готовых потоков и
активизировать их по мере подключения очередного клиента.
Асимметричные потоки (asymmetric threads) – это потоки, решающие
различные задачи и, как правило, не разделяющие совместные ресурсы.
Необходимость в асимметричных потоках возникает:
− когда в программе необходимы длительные вычисления, при этом
необходимо сохранить нормальную реакцию на ввод;
− когда нужно обрабатывать асинхронный ввод/вывод с использованием
различных устройств (СОМ-порта, звуковой карты, принтера и т. п.);
− когда вы хотите создать несколько окон и одновременно обрабатывать
ввод в них.

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

3. Приоритеты потоков
Интерфейс Win32 API позволяет программисту управлять распределением
времени между потоками; это распространяется и на приложения, написанные
на Delphi. Операционная система планирует время процессора в соответствии с
27
приоритетами потоков.
Приоритет потока – величина, складывающаяся из двух составных частей:
приоритета породившего поток процесса и собственно приоритета потока.
Когда поток создается, ему назначается приоритет, соответствующий
приоритету породившего его процесса.
В свою очередь, процессы могут иметь следующие классы приоритетов.
− Real time;
− Normal;
− High;
− Below normal;
− Above normal;
− Idle.
Класс реального времени задает приоритет даже больший, чем у многих
процессов операционной системы. Такой приоритет нужен для процессов,
обрабатывающих высокоскоростные потоки данных. Если такой процесс не
завершится за короткое время, пользователь почувствует, что система
перестала откликаться, т. к. даже обработка событий мыши не получит времени
процессора.
Использование класса High ограничено процессами, которые должны
завершаться за короткое время, чтобы не вызвать сбойной ситуации. Пример –
процесс, который посылает сигналы внешнему устройству; причем устройство
отключается, если не получит своевременный сигнал. Если у вас возникли
проблемы с производительностью вашего приложения, было бы неправильно
решать их просто за счет повышения его приоритета до high – такой процесс
также влияет на всю ОС. Возможно, в этом случае следует модернизировать
компьютер.
Большинство процессов запускается в рамках класса с нормальным
приоритетом. Нормальный приоритет означает, что процесс не требует какого-
либо специального внимания со стороны операционной системы.
И, наконец, процессы с фоновым приоритетом запускаются лишь в том
случае, если в очереди Диспетчера задач нет других процессов. Обычные виды
приложений, использующие такой приоритет, – это программы сохранения
экрана и системные агенты (system agents).
Программисты могут использовать фоновые процессы для организации
завершающих операций и реорганизации данных. Примерами могут служить
сохранение документа или резервное копирование базы данных.
Приоритеты имеют значения от 0 до 31. Процесс, породивший поток,
может впоследствии изменить его приоритет; в этой ситуации программист
имеет возможность управлять скоростью отклика каждого потока.
Нормальная практика для асимметричных потоков – это назначение
потоку, обрабатывающему ввод, более высокого приоритета, а всем остальным
– более низкого или даже приоритета idle, если этот поток должен выполняться
только во время простоя системы.

28
4. Функции работы с потоками
Создается поток функцией CreateThread, которая имеет следующий прототип:
HANDLE CreateThread (
LPSECURITY ATTRIBUTES lpThreadAttributes,// атрибуты защиты
DWORD dwStackSize,// размер стека потока в байтах
LPTHREAD_START_ROUTINE lpStartAddress,// адрес исполняемой
функции
LPVOID lpParameter,// адрес параметра
DWORD dwCreationFlags,// флаги создания потока
LPDWORD lpThreadId// идентификатор потока
) ;
При успешном завершении функция CreateThread возвращает дескриптор
созданного потока и его идентификатор, который является уникальным для
всей системы. В противном случае эта функция возвращает значение NULL.
Кратко опишем назначение параметров функции CreateThread. Параметр
lpThreadAttributes устанавливает атрибуты защиты создаваемого потока. До тех
пор пока мы не изучим структуру системы безопасности в Windows, то есть
раздел Windows NT Access Control из интерфейса программирования
приложений Win32 API, мы будем устанавливать значения этого параметра в
NULL при вызове почти всех функций ядра Windows. Это означает, что
атрибуты защиты потока совпадают с атрибутами защиты создавшего его
процесса. О процессах будет подробно рассказано в следующем разделе.
Параметр dwStackSize определяет размер стека, который выделяется
потоку при запуске. Если этот параметр равен нулю, то потоку выделяется стек,
размер которого равен по умолчанию 1 Мб. Это наименьший размер стека,
который может быть выделен потоку. Если величина параметра dwStackSize
меньше, значения, заданного по умолчанию, то все равно потоку выделяется
стек размеров в 1Мб. Операционная система Windows округляет размер стека
до одной страницы памяти, который обычно равен 4 Кб.
Параметр lpStartAddress указывает на исполняемую потоком функцию. Эта
функция должна иметь следующий прототип:
DWORD WINAPI ThreadProc (LPVOID lpParameters);
Параметр lpParameter является единственным параметром, который будет
передан функции потока.
Параметр dwCreationFlags определяет, в каком состоянии будет создан
поток. Если значение этого параметра равно 0, то функция потока начинает
выполняться сразу после создания потока. Если же значение этого параметра
равно CREATE_SUSPENDED, то поток создается в подвешенном состоянии. В
дальнейшем этот поток можно запустить вызовом функции ResumeThread.
Параметр lpThreadId является выходным, то есть его значение устанавливает
Windows. Этот параметр должен указывать на переменную, в которую Windows
поместит идентификатор потока, который уникален для всей системы и может
в дальнейшем использоваться для ссылок на поток.
Приведем пример программы, которая использует функцию CreateThread
для создания потока, и продемонстрируем способ передачи параметров
исполняемой потоком функции.

29
Листинг 1. Пример создания потока функцией CreateThread
#include <windows.h>
#include <iostream.h>
volatile int n;
DWORD WINAPI Add(LPVOID iNum)
{
cout << "Thread is started." << endl;
n += (int)iNum;
cout << "Thread is finished." << endl; return 0;
}
int main()
{
int inc = 10;
HANDLE hThread;
DWORD IDThread;
cout << "n = " << n << endl;
hThread = CreateThread(NULL, 0, Add, (void*)inc, 0, &IDThread);
if (hThread == NULL)
return GetLastError();
// ждем пока поток Add закончит работу
WaitForSingleObject(hThread, INFINITE);
// закрываем дескриптор потока Add
CloseHandle(hThread);
cout << "n = " << n << endl;
return 0;
}
Отметим, что в этой программе используется функция WaitForSingleObject,
которая ждет завершения потока Add.

Задание на выполнение
А. Изучить программу для консольного процесса, который состоит из двух
потоков: main и worker.
Поток main должен выполнить следующие действия:
1. Создать массив целых чисел, размерность и элементы которого вводятся с
консоли.
2. Создать поток worker.
3. Найти минимальный и максимальный элементы массива и вывести их на
консоль. После каждого сравнения элементов «спать» 7 миллисекунд.
4. Дождаться завершения потока worker.
5. Подсчитать количество элементов в массиве, значение которых больше
среднего значения элементов массива, и вывести его на консоль.
6. Завершить работу.
Поток worker должен выполнить следующие действия:
1. Найти среднее значение элементов массива. После каждого суммирования
элементов «спать» 12 миллисекунд.
2. Завершить свою работу.
Для ожидания завершения работы потока worker использовать функцию:
DWORD WaitForSingleObject(
HANDLE hHandle,// дескриптор объекта
DWORD dwMilliseconds // интервал ожидания в миллисекундах

30
);
где второй параметр установить равным INFINITE. Например
WaitForSingleObject(hThread, INFINITE); // ждать завершения потока
Здесь hThread – дескриптор потока worker.
Для засыпания использовать функцию:
VOID Sleep(
DWORD dwMilliseconds // миллисекунды
);
Например,
Sleep (12); // спать 12 миллисекунд
3. Модифицировать и отладить программу в соответствии со своим
вариантом.

Варианты заданий
1) Поток worker должен найти значение факториала элементов массива.
2) Поток worker должен найти значение суммы четных элементов
массива.
3) Поток worker должен найти значение количество четных элементов
массива.
4) Поток worker должен найти значение количество нечетных элементов
массива.
5) Поток worker должен найти значение суммы нечетных элементов
массива.
6) Поток worker должен найти значение среднее значение четных
элементов массива.
7) Поток worker должен найти значение среднее значение нечетных
элементов массива.
8) Поток worker должен найти значение факториала четных элементов
массива.
9) Поток worker должен найти значение факториала нечетных элементов
массива.
10) Поток worker должен найти значение среднее значение элементов
массива, исключая максимальный элемент.
11) Поток worker должен найти значение среднее значение элементов
массива, исключая минимальный элемент.
12) Поток worker должен найти значение факториала элементов массива,
исключая максимальный элемент.
13) Поток worker должен найти значение факториала элементов массива,
исключая минимальный элемент.
14) Поток worker должен найти значение суммы нечетных элементов
массива и минимального элемента.
15) Поток worker должен найти значение суммы четных элементов
массива и минимального элемента.
16) Поток worker должен найти значение факториала элементов массива.
17) Поток worker должен найти значение суммы четных элементов
массива.
18) Поток worker должен найти значение количество четных элементов
31
массива.
19) Поток worker должен найти значение количество нечетных элементов
массива.
20) Поток worker должен найти значение суммы нечетных элементов
массива.
21) Поток worker должен найти значение среднее значение четных
элементов массива.
22) Поток worker должен найти значение среднее значение нечетных
элементов массива.
23) Поток worker должен найти значение факториала четных элементов
массива.
24) Поток worker должен найти значение факториала нечетных элементов
массива.

Контрольные вопросы
1. Дайте определение понятию поток.
2. Что такое «симметричные» и «асимметричные» потоки. В каких
ситуациях возникает необходимость в асимметричных потоках?
2. Каково различие процессов от потоков?
3. Что такое приоритет потока?
4. Перечислите классы приоритетов для процессов.
5. Каким образом можно добавить новый поток в текущий процесс?

Лабораторная работа №4
Тема: Синхронизация потоков при помощи семафоров и критических секций

Цель работы:
1. Изучить объекты синхронизации потоков семафор и критические секции
2. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью семафоров.
3. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью критических секций.

Краткое теоретическое введение


1. Критические секции в Windows
В операционных системах Windows проблема взаимного исключения для
параллельных потоков, выполняемых в контексте одного процесса, решается
при помощи объекта типа CRITICAL_SECTION, который не является объектом
ядра операционной системы. Для работы с объектами этого типа используются
следующие функции:
VOID InitializeCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
VOID EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
BOOL TryEnterCriticalSection (LPCRITICAL_SECTION
lpCriticalSection);
32
VOID LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
каждая из которых имеет единственный параметр, указатель на объект типа
CRITICAL_SECTION. Все эти функции, за исключением
TryEnterCriticalSection, не возвращают значения. Отметим, что функция
TryEnterCriticalSection поддерживается только операционной системой
Windows 2000.
Кратко рассмотрим порядок работы с этими функциями. Для этого
предположим, что при проектировании программы мы выделили некоторый
разделяемый ресурс и критические секции в параллельных потоках, которые
имеют доступ к этому разделяемому ресурсу. Тогда для обеспечения
корректной работы с этим ресурсом нужно выполнить следующую
последовательность действий:
− определить в нашей программе объект типа CRITICAL_SECTION, имя
которого логически связано с выделенным разделяемым ресурсом;
− проинициализировать объектом типа CRITICAL_SECTION при
помощи функции InitializeCriticalSection;
− в каждом из параллельных потоков пред входом в критическую секцию
вызвать функцию EnterCriticalSection, которая исключает
одновременный вход в критические секции, связанные с нашим
разделяемым ресурсом, для параллельно выполняющихся потоков;
− после завершения работы с разделяемым ресурсом, поток должен
покинуть свою критическую секцию, что выполняется посредством
вызова функции LeaveCriticalSection;
− после окончания работы с объектом типа CRITICAL_SECTION,
необходимо освободить все системные ресурсы, которые
использовались этим объектом. Для этой цели служит функция
DeleteCriticalSection.
Теперь покажем работу этих функций на примере. Для этого сначала
рассмотрим пример, в котором выполняются не синхронизированные
параллельные потоки, а затем синхронизируем их работу, используя
критические секции.

Листинг 1. Пример работы не синхронизированных потоков


#include <windows.h>
#include <iostream>
using namespace std;
DWORD WINAPI thread(LPVOID)
{
int i,j;
for (j = 0; j < 10; j++)
{
for (i = 0; i < 10; i++)
{
cout << j << ' '; cout << flush;
Sleep(22);
}
cout << endl;

33
}
return 0;
}
int main()
{
int i,j;
HANDLE hThread;
DWORD IDThread;
hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL)
return GetLastError();
// так как потоки не синхронизированы,
// то выводимые строки непредсказуемы for (j = 10; j < 20; j++)
{
for (i = 0; i < 10; i++)
{
cout << j << ' '; cout << flush;
Sleep(22);
}
cout << endl;
}
// ждем, пока поток thread закончит свою работу
WaitForSingleObject(hThread, INFINITE);
return 0;
}

В этой программе каждый из потоков main и thread выводит строки


одинаковых чисел. Но из-за параллельной работы потоков, каждая выведенная
строка может содержать не равные между собой элементы. Наша задача будет
заключаться в следующем: нужно так синхронизировать потоки main и thread,
чтобы в каждой строке выводились только равные между собой элементы.
Следующая программа показывает решение этой задачи с помощью объекта
типа CRITICAL_SECTION.

Листинг 2. Пример работы синхронизированных потоков


#include <windows.h>
#include <iostream>
using namespace std;
CRITICAL_SECTION cs;
DWORD WINAPI thread(LPVOID)
{
int ij;
for (j = 0; j < 10; j++)
{
// входим в критическую секцию EnterCriticalSection (&cs);
for (i = 0; i < 10; i++)
{
cout << j << ' '; cout.flush();
}
cout << endl;
// выходим из критической секции
LeaveCriticalSection(&cs);
}
return 0;
34
}
int main()
{
int i,j;
HANDLE hThread;
DWORD IDThread;
// инициализируем критическую секцию
InitializeCriticalSection(&cs);
hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL)
return GetLastError();
// потоки синхронизированы, поэтому каждая
// строка содержит только одинаковые числа
for (j = 10; j < 20; j++)
{
// входим в критическую секцию
EnterCriticalSection(&cs);
for (i = 0; i < 10; i++)
{
cout << j << ' '; cout.flush();
}
cout << endl;
// выходим из критической секции
LeaveCriticalSection(&cs);
}
// закрываем критическую секцию
DeleteCriticalSection(&cs);
// ждем, пока поток thread закончит свою работу
WaitForSingleObject(hThread, INFINITE);
return 0;
}

Теперь рассмотрим использование функции TryEnterCriticalSection. Для


этого просто заменим в приведенной программе вызовы функции
EnterCriticalSection на вызовы функции TryEnterCriticalSection и будем
отмечать успешные входы потоков в свои критические секции. Еще раз
подчеркнем, что функция TryEnterCriticalSection работает только на платформе
операционной системы Windows 2000.

Листинг 3. Пример работы синхронизированных потоков.


// Работает только в Windows 2000.
#include <windows.h>
#include <iostream>
using namespace std;
CRITICAL_SECTION cs;
DWORD WINAPI thread(LPVOID)
{
int ij;
for (j = 0; j < 10; j++)
{
// попытка войти в критическую секцию
TryEnterCriticalSection (&cs);
for (i = 0; i < 10; i++)
35
{
cout << j << " "; cout.flush();
}
cout << endl;
// выход из критической секции
LeaveCriticalSection(&cs);
}
return 0;
}

int main()
{

int i, j;
HANDLE hThread;
DWORD IDThread;
// инициализируем критическую секцию
InitializeCriticalSection(&cs);
hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL)
return GetLastError();
// потоки синхронизированы, поэтому каждая
// строка содержит только одинаковые числа
for (j = 10; j < 20; j++)
{
// попытка войти в критическую секцию
TryEnterCriticalSection(&cs);
for (i = 0; i < 10; i++)
{
cout << j << " "; cout.flush();
}
cout << endl;
// выход из критической секции
LeaveCriticalSection(&cs);
}
// удаляем критическую секцию
DeleteCriticalSection(&cs);
// ждем завершения работы потока thread
WaitForSingleObject(hThread, INFINITE);
return 0;
}

Отметим, что, так как объекты типа CRITICAL_SECTION не являются


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

2. Семафоры Дийкстры
Семафор – это неотрицательная целая переменная, значение которой
может изменяться только при помощи неделимых операций. Под понятием
неделимая операция мы понимаем такую операцию, выполнение которой не
может быть прервано. Семафор считается свободным, если его значение
больше нуля, в противном случае семафор считается занятым. Пусть s -
семафор, тогда над ним можно определить следующие неделимые операции:
P(s) {

36
если s >0 то s = s - 1; // поток продолжает работу
иначе ждать освобождения s; // поток переходит в состояние
ожидания
}
V(s) {
если потоки ждут освобождения s, то освободить один поток;
иначе s = s + 1;
}
Семафор с операциями P и V называется семафором Дийкстры, который
первым использовал семафоры для решения задач синхронизации. Из
определения операций над семафором видно, что если поток выдает операцию
P и значение семафора больше нуля, то значение семафора уменьшается на 1 и
этот поток продолжает свою работу, в противном случае поток переходит в
состояние ожидания до освобождения семафора другим потоком. Вывести из
состояния ожидания поток, который ждет освобождения семафора, может
только другой поток, который выдает операцию V над этим же семафором.
Потоки, ждущие освобождения семафора, выстраиваются в очередь к этому
семафору. Дисциплина обслуживания очереди зависит от конкретной
реализации. Очередь может обслуживаться как по правилу FIFO, так и при
помощи более сложных алгоритмов, учитывая приоритеты потоков.
Семафор, который может принимать только значения 0 или 1, называется
двоичным или бинарным семафором. Чтобы подчеркнуть отличие бинарного
семафора от не бинарного семафора, то есть такого семафора, значение
которого может быть больше 1, последний обычно называют считающими
семафором. Покажем, как бинарный семафор может использоваться для
моделирования критических секций и событий. Для этого сначала рассмотрим
следующие потоки.
semaphor s = 1; // семафор свободен
void thread_1( ) void thread_2( )
{ {
P(s); P(s);
if (n%2 == 0) n++;
n = a; V(s);
else .
n = b; .
V(s); .
. }
}
Как следует из определения операций над семафором, данный подход
решает проблему взаимного исключения одновременного доступа к
переменной n для потоков thread_1 и thread_2. Таким образом, бинарный
семафор позволяет решить проблему взаимного исключения.
Теперь предположим, что поток thread_1 должен производить проверку
значения переменной n только после того, как поток thread_2 увеличит
значение этой переменной. Для решения этой задачи модифицируем наши
программы следующим образом:
semaphor s = 0; // семафор занят
void thread_1( ) void thread_2( )
{ {

37
P(s); n++;
if (n%2 == 0) V(s);
n = a; .
else .
n = b; .
. }
}
Как видно из этих программ, бинарный семафор позволяет также решить
задачу условной синхронизации.

3. Семафоры в Windows
Семафоры в операционных системах Windows описываются объектами
ядра Semaphores, Семафор находится в сигнальном состоянии, если его
значение больше нуля. В противном случае семафор находится в не сигнальном
состоянии. Создаются семафоры посредством вызова функции
CreateSemaphore, которая имеет следующий прототип:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttribute, // атрибуты защиты
LONG lInitialCount, // начальное значение семафора
LONG lMaximumCount, // максимальное значение семафора
LPCTSTR lpName // имя семафора
);
Как и обычно, пока значение параметра lpSemaphoreAttributes будем
устанавливать в NULL. Основную смысловую нагрузку в этой функции несут
второй и третий параметры. Значение параметра lInitialCount устанавливает
начальное значение семафора, которое должно быть не меньше 0 и не больше
его максимального значения, которое устанавливается параметром
lMaximumCount.
В случае успешного завершения функция CreateSemaphore возвращает
дескриптор семафора, в случае неудачи - значение NULL. Если семафор с
заданным именем уже существует, то функция CreateSemaphore возвращает
дескриптор этого семафора, а функция GetLastError, вызванная после функции
CreateSemaphore вернет значение ERROR_ALREADY_EXISTS.
Значение семафора уменьшается на 1 при его использовании в функции
ожидания. Увеличить значение семафора можно посредством вызова функции
ReleaseSemaphore, которая имеет следующий прототип:
BOOL Release Semaphore(
HANDLE hSemaphore, // дескриптор семафора
LONG lRelease Count, // положительное число,
// на которое увеличивается значение семафора LPLONG
lpPreviousCount // предыдущее значение семафора
);
В случае успешного завершения функция ReleaseSemaphore возвращает
значение TRUE, в случае неудачи - FALSE. Если значение семафора плюс
значение параметра lReleaseCount
больше максимального значения семафора, то функция ReleaseSemaphore
возвращает значение FALSE и значение семафора не изменяется.
Значение параметра lpPreviousCount этой функции может быть равно
NULL. В этом случае предыдущее значение семафора не возвращается.
38
Приведем пример программы, в которой считающий семафор используется
для синхронизации работы потоков. Для этого сначала рассмотрим не
синхронизированный вариант этой программы.

Листинг 4. Несинхронизированные потоки


#include <windows.h>
#include <iostream>
using namespace std;
volatile int a[10];
DWORD WINAPI thread(LPVOID)
{
int i;
for (i = 0; i < 10; i++)
{
a[i] = i + 1;
Sleep(17);
}
return 0;
}
int main()
{
int i;
HANDLE hThread;
DWORD IDThread;
cout << "An initial state of the array: ";
for (i = 0; i < 10; i++)
cout << a[i] <<' ';
cout << endl;
// создаем поток, который готовит элементы массива
hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread);
if (hThread == NULL)
return GetLastError();
// поток main выводит элементы массива
cout << "A modified state of the array: ";
for (i = 0; i < 10; i++)
{
cout << a[i] << ' '; cout.flush();
Sleep(17);
}
cout << endl;
CloseHandle(hThread);
return 0;
}
Теперь кратко опишем работу этой программы. Поток thread
последовательно присваивает элементам массива «a» значения, которые на
единицу больше чем их индекс.
Поток main последовательно выводит элементы массива «а» на консоль.
Так как потоки thread и main не синхронизированы, то неизвестно, какое
состояние массива на консоль поток main. Наша задача состоит в том, чтобы
поток main выводил на консоль элементы массива «a» сразу после их
подготовки потоком thread. Для этого мы используем считающий семафор.
Следующая программа показывает, как этот считающий семафор используется
39
для синхронизации работы потоков.

Листинг 5. Пример синхронизации потоков с использованием семафора


#include <windows.h>
#include <iostream>
using namespace std;
volatile int a[10];
HANDLE hSemaphore;
DWORD WINAPI thread(LPVOID)
{
int i;
for (i = 0; i < 10; i++)
{
a[i] = i + 1;
// отмечаем, что один элемент готов
ReleaseSemaphore(hSemaphore, 1 ,NULL);
Sleep(500);
}
return 0;
}

int main()
{

int i;
HANDLE hThread;
DWORD IDThread;
cout << "An initial state of the array: ";
for (i = 0; i < 10; i++)
cout << a[i] <<' '; cout << endl;
// создаем семафор
hSemaphore=CreateSemaphore(NULL, 0, 10, NULL); if (hSemaphore
== NULL)
return GetLastErrorO;
// создаем поток, который готовит элементы массива hThread =
CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread ==
NULL)
return GetLastError();
// поток main выводит элементы массива // только после их
подготовки потоком thread cout << "A final state of the array: ";
for (i = 0; i < 10; i++)
{
WaitForSingleObject(hSemaphore, INFINITE);
cout << a[i] << ' ';
cout.flush();
}
cout << endl;
CloseHandle(hSemaphore);
CloseHandle(hThread);
return 0;
}
Может возникнуть следующий вопрос: почему для решения этой задачи
используется именно считающий семафор и почему его максимальное значение
равно 10. Конечно, поставленную задачу можно было бы решить и другими
способами. Но дело в том, что считающие семафоры предназначены именно
40
для решения подобных задач. Подробнее, считающие семафоры используются
для синхронизации доступа к однотипным ресурсам, которые производятся
некоторым потоком или несколькими потоками, а потребляются другим
потоком или несколькими потоками. В этом случае значение считающего
семафора равно количеству произведенных ресурсов, а его максимальное
значение устанавливается равным максимально возможному количеству таких
ресурсов. При производстве единицы ресурса значение семафора увеличивается
на единицу, а при потреблении единицы ресурса значение семафора
уменьшается на единицу. В нашем примере ресурсами являются элементы
массива, заполненные потоком thread, который является производителем этих
ресурсов. В свою очередь поток main является потребителем этих ресурсов,
которые он выводит на консоль. Так как в общем случае мы не можем сделать
предположений о скоростях работы параллельных потоков, то максимальное
значение считающего семафора должно быть установлено в максимальное
количество производимых ресурсов. Если поток потребитель ресурсов работает
быстрее чем поток производитель ресурсов, то, вызвав функцию ожидания
считающего семафора, он вынужден будет ждать, пока поток- производитель не
произведет очередной ресурс. Если же наоборот, поток-производитель работает
быстрее чем поток-потребитель, то первый поток произведет все ресурсы и
закончит свою работу, не ожидая, пока второй поток потребит их. Такая
синхронизация потоков производителей и потребителей обеспечивает их
максимально быструю работу.
Доступ к существующему семафору можно открыть с помощью одной из
функций CreateSemaphore или OpenSemaphore. Если для этой цели
используется функция CreateSemaphore, то значения параметров lInitialCount и
lMaximalCount этой функции игнорируются, так как они уже установлены
другим потоком, а поток, вызвавший эту функцию, получает полный доступ к
семафору с именем, заданным параметром lpName. Теперь рассмотрим
функцию OpenSemaphore, которая используется в случае, если известно, что
семафор с заданным именем уже существует. Эта функция имеет следующий
прототип:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, // флаги доступа
BOOL bInheritHandle, // режим наследования
LPCTSTR lpName // имя события
);
Параметр dwDesiredAccess определяет доступ к семафору, и может
быть равен любой логической комбинации следующих флагов:
SEMAPHORE_ALL_ACCESS
SEMAPHORE_MODIFY_STATE
SYNCHRONIZE
Флаг SEMAPHORE_ALL_ACCESS устанавливает для потока полный
доступ к семафору. Это означает, что поток может выполнять над семафором
любые действия. Флаг SEMAPHORE_MODIFY_STATE означает, что поток
может использовать только функцию ReleaseSemaphore для изменения значения
семафора. Флаг SYNCHRONIZE означает, что поток может использовать
семафор только в функциях ожидания. Отметим, что последний режим

41
поддерживается только на платформе Windows NT/2000.

Задание на выполнение
Написать программу для консольного процесса, который состоит из трёх
потоков: main , work, и третьего (см. варианты).
Глобальные переменные не использовать!
Индивидуальные варианты:
1. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль
каждого нового элемента массива.
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут выведены на консоль k элементов
массива).
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− найти в массиве неповторяющиеся элементы (разместить их в массиве
слева, остальные соответственно справа). Элементы - символы.
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком main, использовать бинарный семафор!):
− ждёт от потока main сигнал о начале суммирования;
− выполнить суммирование элементов итогового массива до заданной
позиции k;
− вывести итоговую сумму.
2. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток SumElement;
− освободить выходной поток stdout после вывода на консоль каждого
42
нового элемента массива.
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу используя бинарный
семафор для синхронизации с потоком main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− найти в массиве повторяющиеся элементы (разместить их группы в
массиве слева, остальные соответственно справа). Элементы –
вещественные числа.
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будет сформирован итоговый массив.
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать
− ждёт от потока work сигнал о начале суммирования;
− выполнить суммирование элементов итогового массива;
− вывести итоговую сумму.
3. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива.
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− -известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут выведены на консоль k элементов).
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− cортировка методом “пузырька”. Элементы - вещественные числа
двойной точности;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени.
Поток SumElement должен выполнить следующие действия (Для
43
синхронизации с потоком main, использовать бинарный семафор!):
− ждёт от потока main сигнал о начале суммирования;
− выполнить суммирование элементов итогового массива до заданной
позиции k;
− вывести итоговую сумму.
4. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток MultElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work.
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main – использовать семафор. Проверить работу используя бинарный
семафор для синхронизации с потоком main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве элементов из диапазона [A,B] (разместить их в массиве
слева, остальные элементы массива - заполнить нулями). Элементы -
целые числа без знака. Числа A,B ввести в потоке main.
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток MultElement о начале работы (момент запуска
произойдёт после того, будет сформирована часть итогового массива
(когда будут найдены все элементы из диапазона [A, B]).
Поток MultElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать критическую секцию!):
− ждёт от потока work сигнал о начале работы;
− выполнить произведение элементов итогового массива (когда будут
найдены все элементы из диапазона [A, B]);
− вывести произведение.
5. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
44
параллельно с работой потока work;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут выведены на консоль k элементов
массива).
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!)'.
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− сортировка выбором. Элементы - символы;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени.
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком main, использовать бинарный семафор!)'
− ждёт от потока main сигнал о начале суммирования;
− выполнить суммирование элементов (кодов символов) итогового
массива до заданной позиции k;
− вывести итоговую сумму.
6. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− запросить число А;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work.
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу используя бинарный
семафор для синхронизации с потоком main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве элементов >А (разместить их в массиве слева,
остальные элементы массива -заполнить нулями). Элементы - целые
числа без знака. Число А ввести в потоке main;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будет сформирован итоговый массив.
Поток SumElement должен выполнить следующие действия (Для
45
синхронизации с потоком work, использовать критическую секцию!):
− от потока work сигнал о начале суммирования;
− выполнить суммирование элементов итогового массива;
− вывести итоговую сумму.
7. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут готовы к печати k - элементов массива).
Поток work должен выполнить следующие действия Для синхронизации с
потоком main – использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!.
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− Поиск в массиве простых чисел (разместить их в массиве слева,
остальные элементы массива - справа). Элементы - целые числа без
знака.
− извещать поток main о новом элементе, после каждого готового
элемента отдыхать в течение заданного интервала времени;
− поток SumElement должен выполнить следующие действия Для
синхронизации с потоком main, использовать бинарный семафор!
− ждёт от потока main сигнал о начале суммирования;
− выполнить суммирование элементов итогового массива до заданной
позиции k, вывести итоговую сумму.
8. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток CountElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− запросить символ X;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− поток work должен выполнить следующие действия (Для
синхронизации с потоком main - использовать семафор. Проверить
46
работу используя бинарный семафор для синхронизации с потоком
main, объяснить отличия, если есть!)
− запросить у пользователя временной интервал, требуемый для
отдыха после подготовки одного элемента в массиве;
− поиск в массиве элементов =Х (разместить их в массиве слева,
остальные элементы массива -справа). Элементы - символы. X ввести в
потоке main;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток CountElement о начале работы (момент запуска
произойдёт после того, будет сформирован итоговый массив.
Поток CountElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать критическую секцию!)
− ждёт от потока work сигнал о начале суммирования;
− подсчитать количество элементов равных X;
− вывести итоговую сумму.
9. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток MultElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− известить поток MultElement о начале работы (момент запуска
произойдёт после того, будут выведены на консоль k элементов
массива).
− поток work должен выполнить следующие действия (Для
синхронизации с потоком main - использовать семафор. Проверить
работу программы используя критическую секцию для синхронизации
с потоком main, объяснить отличия, если есть!)
− запросить у пользователя временной интервал, требуемый для
отдыха после подготовки одного элемента в массиве;
− поиск в массиве элементов <А (разместить их в массиве слева,
остальные элементы массива - справа). Элементы - вещественные числа.
Число А ввести в потоке main;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− поток MultElement должен выполнить следующие действия (Для
синхронизации с потоком main, использовать бинарный семафор!)
− ждёт от потока main сигнал о начале суммирования;
47
− выполнить произведение элементов итогового массива до заданной
позиции k;
− вывести итоговое произведений.
10. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива.
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− поток work должен выполнить следующие действия (Для
синхронизации с потоком main - использовать семафор. Проверить
работу используя бинарный семафор для синхронизации с потоком
main, объяснить отличия, если есть!)
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве лексем, (разделители - цифры). Полученные лексемы
поместить в массиве слева, разделитель - пробел, остальные элементы -
заполнить символом ‘0’. Элементы массива - символы;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будет сформирован итоговый массив;
− поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать критическую секцию!)
− ждёт от потока work сигнал о начале суммирования;
− выполнить суммирование элементов (кодов) итогового массива;
− вывести итоговую сумму.
11 . Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут выведены на консоль k элементов
массива);
48
− поток work должен выполнить следующие действия (Для
синхронизации с потоком main - использовать семафор. Проверить
работу используя бинарный семафор для синхронизации с потоком
main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− приведение массива к палиндрому (получившейся палиндром
поместить в массиве слева, а лишние элементы соответственно - справа )
Элементы – символы извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком main, использовать критическую секцию!):
− ждёт от потока main сигнал о начале суммирования;
− выполнить суммирование элементов (кодов) итогового массива до
заданной позиции k;
− вывести итоговую сумму.
12. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток MultElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве; сортировка выбором.
Элементы - целые числа, извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток MultElement о начале работы (момент запуска
произойдёт после того, будет сформирован итоговый массив.
Поток MultElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать бинарный семафор!):
− ждёт от потока work сообщения о начале суммирования;
− выполнить произведение элементов итогового массива;
− вывести произведение.
13. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
49
− вывести размерность и элементы исходного массива на консоль;
− ввести число k;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будут выведены на консоль k элементов
массива).
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу программы используя
критическую секцию для синхронизации с потоком main, объяснить отличия,
если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве элементов, соответствующих цифрам (слева поместить
в массив цифры, а остальные элементы массива - заполнить
пробелами). Элементы - символы.
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком main, использовать бинарный семафор!):
− ждёт от потока main сообщения о начале суммирования;
− выполнить суммирование элементов (кодов) итогового массива до
заданной позиции k;
− вывести итоговую сумму.
14. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток SumElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main – использовать семафор. Проверить работу используя бинарный
семафор для синхронизации с потоком main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве лексем, начинающихся с цифры (разделители - пробел и
тире). Полученные лексемы поместить в массиве слева, а лишние
50
элементы заполнить символом подчеркивания: «_» ). Элементы –
символы;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток SumElement о начале суммирования (момент запуска
произойдёт после того, будет сформирован итоговый массив.
Поток SumElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать критическую секцию!):
− ждёт от потока work сообщения о начале суммирования;
− выполнить суммирование элементов (кодов) итогового массива;
− вывести итоговую сумму.
15. Поток main должен выполнить следующие действия:
− создать массив, размерность и элементы которого вводятся
пользователем с консоли;
− вывести размерность и элементы исходного массива на консоль;
− запустить поток work;
− запустить поток Sum/CountElement;
− освобождение выходной поток stdout после вывода на консоль каждого
нового элемента массива;
− выводить на экран поэлементно элементы массива (итогового)
параллельно с работой потока work;
Поток work должен выполнить следующие действия (Для синхронизации с
потоком main - использовать семафор. Проверить работу используя бинарный
семафор для синхронизации с потоком main, объяснить отличия, если есть!):
− запросить у пользователя временной интервал, требуемый для отдыха
после подготовки одного элемента в массиве;
− поиск в массиве лексем, начинающихся с цифры (разделители – пробел
и тире).
− полученные лексемы поместить в массиве слева, а лишние элементы
заполнить символом подчеркивания: «_» ). Элементы – символы;
− извещать поток main о новом элементе;
− после каждого готового элемента отдыхать в течение заданного
интервала времени;
− известить поток Sum/CountElement о начале суммирования (момент
запуска произойдёт после того, будет сформирован итоговый массив;
− поток Sum/CountElement должен выполнить следующие действия (Для
синхронизации с потоком work, использовать критическую секцию!):
− ждёт от потока work сообщения о начале суммирования;
− выполнить суммирование и подсчёт элементов (до символов
подчеркивания: «_») итогового массива;
− вывести результаты.

51
Лабораторная работа №5
Тема: Синхронизация процессов при помощи событий и мьютексов

Цель работы:
1. Изучить объекты синхронизации потоков мьютексы и события.
2. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью мьютексов и критических
секций.
3. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью

Краткое теоретическое введение


1. Объекты синхронизации и функции ожидания в Windows
В операционных системах Windows объектами синхронизации
называются объекты ядра, которые могут находиться в одном из двух
состояний: сигнальном (signaled) и несигнальном (nonsignaled). Объекты
синхронизации могут быть разбиты на три класса. К первому классу относятся
объекты синхронизации, которые служат только для решения проблемы
синхронизации параллельных потоков. К таким объектам синхронизации в
Windows относятся:
− мьютекс (mutex);
− событие (event);
− семафор (semaphore).
Ко второму классу объектов синхронизации относится ожидающий таймер
(waitable timer). К третьему классу объектов синхронизации относятся объекты,
которые переходят в сигнальное состояние по завершении своей работы или
при получении некоторого сообщения. Примерами таких объектов
синхронизации являются потоки и процессы. Пока эти объекты выполняются,
они находятся в несигнальном состоянии. Если выполнение этих объектов
заканчивается, то они переходят в сигнальное состояние.
Теперь перейдем к функциям ожидания. Функции ожидания в Windows это
такие функции, параметрами которых являются объекты синхронизации. Эти
функции обычно используются для блокировки потоков, которая выполняется
следующим образом. Если дескриптор объекта синхронизации является
параметром функции ожидания, а сам объект синхронизации находится в
несигнальном состоянии, то поток, вызвавший эту функцию ожидания,
блокируется до перехода этого объекта синхронизации в сигнальное состояние.
Сейчас мы будем использовать только две функции ожидания
WaitForSingleObject и WaitForMultipleObject.
Для ожидания перехода в сигнальное состояние одного объекта
синхронизации используется функция WaitForSingleObject, которая имеет
следующий прототип:
DWORD WaitForSingleObject(
HANDLE hHandle, // дескриптор объекта
DWORD dwMilliseconds // интервал ожидания в миллисекундах
);

52
Функция WaitForSingleObject в течение интервала времени, равного значению
параметра dwMilliseconds, ждет пока объект синхронизации с дескриптором
hHandle перейдет в сигнальное состояние. Если значение параметра
dwMilliseconds равно нулю, то функция только проверяет состояние объекта.
Если же значение параметра dwMilliseconds равно INFINITE, то функция ждет
перехода объекта синхронизации в сигнальное состояние бесконечно долго.
В случае удачного завершения функция WaitForSingleObject возвращает
одно из следующих значений:
WAIT_OBJECT_0
WAIT_ABANDONED
WAIT_TIMEOUT
Значение WAIT_OBJECT_0 означает, что объект синхронизации находился или
перешел в сигнальное состояние. Значение WAIT_ABANDONED означает, что
объектом синхронизации является мьютекс, который не был освобожден
потоком, завершившим свое исполнение. После завершения потока этот
мьютекс освободился системой и перешел в сигнальное состояние. Такой
мьютекс иногда называется забытым мьютексом (abandoned mutex). Значение
WAIT_TIMEOUT означает, что время ожидания истекло, а объект
синхронизации не перешел в сигнальное состояние. В случае неудачи функция
WaitForSingleObject возвращает значение WAIT_FAILED.
Приведем пример простой программы, которая использует функцию
WaitForSingleObject для ожидания завершения потока. Отметим также, что эта
функция уже использовалась нами в Программе 2.1 для ожидания завершения
работы потока Add.

Листинг 1. Пример использования функциеи WaitForSingleObject


#include <windows.h>
#include <iostream>
using namespace std;
void thread()
{
int i;
for (i = 0; i < 10 ; i++)
{
cout << i << ' '; cout << flush << '\a';
Sleep(500);
}
cout << endl;
}

int main()
{

HANDLE hThread;
DWORD dwThread;
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread,
NULL, 0, &dwThread); if (hThread == NULL)
return GetLastError();
// ждем завершения потока thread
if(WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0)
{
cout << "Wait for single object failed." << endl; cout <<

53
"Press any key to exit." << endl;
}
// закрываем дескриптор потока thread CloseHandle(hThread);
return 0;
}

Для ожидания перехода в сигнальное состояние нескольких


объектов синхронизации или одного из нескольких объектов
синхронизации используется функция WaitForMultipleObject, которая
имеет следующий прототип:
DWORD WaitForMultipleObjects(
DWORD nCount, // количество объектов
CONST HANDLE *lpHandles, // массив дескрипторов
объектов
BOOL bWaitAll, // режим ожидания
DWORD dwMilliseconds // интервал ожидания в миллисекундах
);
Функция WaitForMultipleObjects работает следующим образом. Если значение
параметра bWaitAll равно TRUE, то эта функция в течение интервала времени,
равного значению параметра dwMilliseconds, ждет пока все объекты
синхронизации, дескрипторы которых заданы в массиве lpHandles, перейдут в
сигнальное состояние. Если же значение параметра bWaitAll равно FALSE, то
эта функция в течение заданного интервала времени ждет пока любой из
заданных объектов синхронизации перейдет в сигнальное состояние. Если
значение параметра dwMilliseconds равно нулю, то функция только проверяет
состояние объектов синхронизации. Если же значение параметра
dwMilliseconds равно INFINITE, то функция ждет перехода объектов
синхронизации в сигнальное состояние бесконечно долго. Количество объектов
синхронизации, ожидаемых функцией WaitForMultipleObjects, не должно
превышать значения MAXIMUM_WAIT_OBJECTS. Также отметим, что
объекты синхронизации не должны повторяться.
В случае успешного завершения функция WaitForMultipleObjects возвращает
их следующих значений:
от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount - 1); от
WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1);
WAIT_TIMEOUT.
Интерпретация значений, возвращаемых функцией WaitForMultipleObjects,
зависит от значения входного параметра bWaitAll. Сначала рассмотрим случай,
когда значение этого параметра равно TRUE. Тогда возвращаемые значения
интерпретируются следующим образом:
- любое из возвращаемых значений, находящихся в
диапазоне от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount -
1), означает, что все объекты синхронизации находились или перешли в
сигнальное состояние;
- любое из возвращаемых значений, находящихся в диапазоне от
WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1) означает,
что все объекты синхронизации находились или перешли в сигнальное
состояние и, по крайней мере, один их них был забытым мьютексом;
- возвращаемое значение WAIT_TIMEOUT означает, что время ожидания
истекло и не все объекты синхронизации перешли в сигнальное состояние.
54
Теперь рассмотрим случай, когда значение входного параметра bWaitAll
равно FALSE. В этом случае значения, возвращаемые функцией
WaitForMultipleObjects, интерпретируются следующим образом:
- любое из возвращаемых значений, находящихся в
диапазоне от WAIT_OBJECT_0 до
(WAIT_OBJECT_0 + nCount - 1), означает, что, по крайней мере, один из
объектов синхронизации находился или перешёл в сигнальное состояние.
Индекс дескриптора этого объекта в массиве определяется как разница между
возвращаемым значением и величиной WAIT_OBJECT_0;
- любое из возвращаемых значений, находящихся в диапазоне от
WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1) означает, что
одним из объектов синхронизации, перешедшим в сигнальное состояние,
является забытый мьютекс. Индекс дескриптора этого мьютекса в массиве
определяется как разница между возвращаемым значением и величиной
WAIT_OBJECT_0;
- возвращаемое значение WAIT_TIMEOUT означает, что время ожидания
истекло, и ни один из объектов синхронизации не перешел в сигнальное
состояние.
В случае неудачи функция WaitForMultipleObjects возвращает значение
WAIT_FAILED.
Приведем пример программы, которая использует функцию
WaitForSingleObject для ожидания завершения двух потоков.

Листинг 2. Пример использования функциеи WaitForMultipleObjects


#include <windows.h>
#include <iostream>
using namespace std;
void thread_0()
{
int i;
for (i = 0; i < 5 ; i++)
{
cout << i << ' '; cout << flush << '\a';
Sleep(500);
}
cout << endl;
}

void thread_1()
{

int i;
for (i = 5; i < 10 ; i++)
{
cout << i << ' ';
cout << flush << '\a';
Sleep(500);
}
cout << endl;
}

int main()
{
55
HANDLE hThread[2];
DWORD dwThread[2];
// запускаем первый поток
hThread[0] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)thread_0,
NULL, 0, &dwThread[0]);
if (hThread[0] == NULL)
return GetLastError();
// запускаем второй поток
hThread[1] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)thread_1,
NULL, 0, &dwThread[1]);
if (hThread[1] == NULL)
return GetLastError();
// ждем завершения потоков thread_1 и thread_2
if (WaitForMultipleObjects(2, hThread, TRUE, INFINITE) ==
WAIT_F AILED)
{
cout << "Wait for multiple objects failed." << endl; cout <<
"Press any key to exit." << endl;
}
// закрываем дескрипторы потоков thread_0 и thread_1
CloseHandle(hThread[0]);
CloseHandle(hThread[ 1 ]);
return 0;
}

Любой ресурс, на доступ к которому претендуют не менее двух


параллельных потоков, называется критическим или разделяемым ресурсом.
Участок программы, на протяжении которого поток ведет работу с
критическим ресурсом, называется критической секцией по отношению к этому
ресурсу. Например, рассмотрим два параллельных потока:
Поток 1. Поток 2.
void thread_1( ) void thread_2( )
{ {
if (n%2 == 0) ++n;
n = a; .
else .
n = b; .
. }
}
Возможно, что после проверки условия (n%2 == 0) работа первого потока
прервется, и процессорное время будет передано второму потоку. Второй поток
увеличит значение переменной n на единицу и после этого процессор опять
будет передан первому потоку. В этом случае первый поток присвоит
переменной n неправильное значение. Для исключения такой ситуации,
необходимо блокировать одновременный доступ потоков к переменной n.
Следовательно, в этом примере переменная n или, более точно, область памяти,
занимаемая этой переменной, является критическим ресурсом, а
рассматриваемые участки программного кода являются критическими
секциями по отношению к этому ресурсу. Для правильной работы потоков
thread_1 и thread_2 необходимо обеспечить, чтобы приведенные участки
56
программного кода не могли работать одновременно. Другими словами нам
необходимо решить задачу исключения взаимного доступа потоков thread_1 и
thread_2 к критическому ресурсу, которым является переменная n.
В общем случае проблема взаимного исключения формулируется
следующим образом. Необходимо обеспечить такую работу параллельных
потоков с критическим ресурсом, при которой гарантируется, что критические
секции этих потоков по отношению к этому ресурсу не работают
одновременно.

2. Мьютексы в Windows
Для решения проблемы взаимного исключения между параллельными
потоками, выполняющимися в контексте разных процессов, в операционных
системах Windows используется объект ядра мьютекс. Слово мьютекс является
переводом английского слова mutex, которое в свою очередь является
сокращением от выражения mutual exclusion, что на русском языке значит
взаимное исключение. Мьютекс находится в сигнальном состоянии, если он не
принадлежит ни одному потоку. В противном случае мьютекс находится в
несигнальном состоянии. Одновременно мьютекс может принадлежать только
одному потоку.
Создается мьютекс вызовом функции CreateMutex, которая имеет
следующий прототип:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибуты
защиты
BOOL bInitialOwner, // начальный владелец мьютекса
LPCTSTR lpName // имя мьютекса
);
Пока значение параметра LPSECURITY_ATTRIBUTES будем устанавливать в
NULL. Это означает, что атрибуты защиты заданы по умолчанию, то есть
дескриптор мьютекса не наследуется и доступ к мьютексу имеют все
пользователи. Теперь перейдем к другим параметрам.
Если значение параметра bInitialOwner равно TRUE, то мьютекс сразу
переходит во владение потоку, которым он был создан. В противном случае
вновь созданный мьютекс свободен. Поток, создавший мьютекс, имеет все
права доступа к этому мьютексу.
Значение параметра lpName определяет уникальное имя мьютекса для всех
процессов, выполняющихся под управлением операционной системы. Это имя
позволяет обращаться к мьютексу из других процессов, запущенных под
управлением этой же операционной системы. Длина имени не должна
превышать значение MAX_PATH. Значением параметра lpName может быть
пустой указатель NULL. В этом случае система создает безымянный мьютекс.
Отметим также, что имена мьютексов являются чувствительными к нижнему и
верхнему регистрам.
В случае удачного завершения функция CreateMutex возвращает
дескриптор созданного мьютекса. В случае неудачи эта функция возвращает
значение NULL. Если мьютекс с заданным именем уже существует, то функция
CreateMutex возвращает дескриптор этого мьютекса, а функция GetLastError,
57
вызванная после функции CreateMutex вернет значение
ERROR_ALREADY_EXISTS.
Мьютекс захватывается потоком посредством любой функции ожидания, а
освобождается функцией ReleaseMutex, которая имеет следующий прототип:
BOOL ReleaseMutex( HANDLE hMutex // дескриптор мьютекса
);
В случае успешного завершения функция ReleaseMutex возвращает значение
TRUE, в случае неудачи - FALSE. Если поток освобождает мьютекс, которым
он не владеет, то функция ReleaseMutex возвращает значение FALSE.
Для доступа к существующему мьютексу поток может использовать одну из
функций CreateMutex или OpenMutex. Функция CreateMutex используется в тех
случаях, когда поток не знает, создан или нет мьютекс с указанным именем
другим потоком. В этом случае значение параметра bInitialOwner нужно
установить в FALSE, так как невозможно определить какой из потоков создает
мьютекс. Если поток использует для доступа к уже созданному мьютексу
функцию CreateMutex, то он получает полный доступ к этому мьютексу. Для
того чтобы получить доступ к уже созданному мьютексу, поток может также
использовать функцию OpenMutex, которая имеет следующий прототип:
HANDLE OpenMutex(
DWORD dwDesiredAccess, // доступ к мьютексу
BOOL bInheritHandle // свойство наследования
LPCTSTR lpName // имя мьютекса
Параметр dwDesiredAccess этой функции может принимать одно из
двух значений:
MUTEX_ALL_ACCES S SYNCHRONIZE
В первом случае поток получает полный доступ к мьютексу. Во втором случае
поток может использовать мьютекс только в функциях ожидания, чтобы
захватить мьютекс, или в функции ReleaseMutex, для его освобождения.
Параметр bInheritHandle определяет свойство наследования мьютекса. Если
значение этого параметра равно TRUE, то дескриптор открываемого мьютекса
является наследуемым. В противном случае - дескриптор не наследуется.
В случае успешного завершения функция OpenMutex возвращает дескриптор
открытого мьютекса, в случае неудачи эта функция возвращает значение
NULL.
Покажем пример использования мьютекса для синхронизации потоков из
разных процессов. Для этого сначала рассмотрим пример не
синхронизированных потоков.

Листинг 3. Не синхронизированные потоки, выполняющиеся в разных


процессах
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
int ij;
for (j = 10; j < 20; j++)
{
for (i = 0; i < 10; i++)
58
{
cout << j << ' '; cout.flush();
Sleep(5);
}
cout << endl;
}
return 0;
}

Листинг 4. Не синхронизированные потоки, выполняющиеся в разных


процессах
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{

char lpszAppName[] = "D:\\os.exe";


STARTUPINFO si;
PROCE S S_INF ORM ATION pi;
ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb =
sizeof(STARTUPINFO);
// создаем новый консольный процесс
if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE,
NULL, NULL, NULL, &si, &pi))
{
cout << "The new process is not created." << endl;
cout << "Press any key to exit." << endl; cin.get();
return GetLastError();
}
// выводим на экран строки for (int j = 0; j < 10; j++)
{
for (int i = 0; i < 10; i++)
{
cout << j << ' '; cout.flush();
Sleep(10);
}
cout << endl;
}
// ждем пока дочерний процесс закончит работу
WaitForSingleObject(pi.hProcess, INFINITE);
// закрываем дескрипторы дочернего процесса в текущем процессе
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

Кратко опишем работу этих программ. Вторая из них запускает первую


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

59
Листинг 5. Синхронизация потоков, выполняющихся в // разных
процессах, с использованинм мьютекса
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{

HANDLE hMutex;
int ij;
// открываем мьютекс
hMutex = OpenMutex(SYNCHRONIZE, FALSE, "DemoMutex"); if (hMutex
== NULL)
{
cout << "Open mutex failed." << endl; cout << "Press any key to
exit." << endl; cin.get();
return GetLastError();
}
for (j = 10; j < 20; j++)
{
// захватываем мьютекс WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < 10; i++)
{
cout << j << ' ';
cout.flush();
Sleep(5);
}
cout << endl;
// освобождаем мьютекс ReleaseMutex(hMutex);
}
// закрываем дескриптор объекта CloseHandle(hMutex);
return 0;
}

Листинг 6. Пример синхронизации потоков, выполняющихся в разных


процессах, с использованием мьютекса
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
HANDLE hMutex;
char lpszAppName[] = "D:\\os.exe";
STARTUPINFO si;
PROCESS_INFORMATION pi;
// создаем мьютекс
hMutex = CreateMutex(NULL, FALSE, "DemoMutex"); if (hMutex ==
NULL)
{
cout << "Create mutex failed." << endl; cout << "Press any key
to exit." << endl; cin.get();
return GetLastError();
}
ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb =
sizeof(STARTUPINFO);
// создаем новый консольный процесс
60
if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, NULL,
NULL, NULL, &si, &pi))
{
cout << "The new process is not created." << endl; cout <<
"Press any key to exit." << endl; cin.get();
return GetLastError();
}
// выводим на экран строки for (int j = 0; j < 10; j++)
{
// захватываем мьютекс W aitForSingleObj ect(hMutex, INFINITE);
for (int i = 0; i < 10; i++)
{
cout << j << ' '; cout.flush();
Sleep(10);
}
cout << endl;
// освобождаем мьютекс ReleaseMutex(hMutex);
}
// закрываем дескриптор мьютекса CloseHandle(hMutex);
// ждем пока дочерний процесс закончит работу W aitForSingleObj
ect(pi.hProcess, INFINITE);
// закрываем дескрипторы дочернего процесса в текущем процессе
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}

3. События в Windows
Событием называется оповещение о некотором выполненном действии. В
программировании события используются для оповещения одного потока о
том, что другой поток выполнил некоторое действие. Сама же задача
оповещения одного потока о некотором действии, которое совершил другой
поток называется задачей условной синхронизации или иногда задачей
оповещения.
В операционных системах Windows события описываются объектами ядра
Events. При этом различают два типа событий:
- события с ручным сбросом;
- события с автоматическим сбросом.
Различие между этими типами событий заключается в том, что событие с
ручным сбросом можно перевести в несигнальное состояние только
посредством вызова функции ResetEvent, а событие с автоматическим сбросом
переходит в несигнальное состояние как при помощи функции ResetEvent, так и
при помощи функции ожидания. При этом отметим, что если события с
автоматическим сбросом ждут несколько потоков, используя функцию
WaitForSingleObject, то из состояния ожидания освобождается только один из
этих потоков. Создаются события вызовом функции CreateEvent, которая имеет
следующий прототип:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpSecurity Attributes, // атрибуты
защиты
BOOL bManualReset, // тип события
BOOL bInitialState, // начальное состояние события
61
LPCTSTR lpName // имя события
);
Как и обычно, пока значение параметра lpSecurity Attributes будем
устанавливать в NULL. Основную смысловую нагрузку в этой функции несут
второй и третий параметры. Если значение параметра bManualReset равно
TRUE, то создается событие с ручным сбросом, в противном случае - с
автоматическим сбросом. Если значение параметра bInitialState равно TRUE, то
начальное состояние события является сигнальным, в противном случае -
несигнальным. Параметр lpName задает имя события, которое позволяет
обращаться к нему из потоков, выполняющихся в разных процессах. Этот
параметр может быть равен NULL, тогда создается безымянное событие.
В случае удачного завершения функция CreateEvent возвращает дескриптор
события, а в случае неудачи - значение NULL. Если событие с заданным
именем уже существует, то функция CreateEvent возвращает дескриптор этого
события, а функция GetLastError, вызванная после функции CreateEvent вернет
значение ERROR_ALREADY_EXISTS.
Ниже приведена программа, в которой безымянные события с
автоматическим сбросом используются для синхронизации работы потоков,
выполняющихся в одном процессе.

Листинг 7. Пример синхронизации потоков при помощи событий с


автоматическим сбросом
#include <windows.h>
#include <iostream>
using namespace std; volatile int n;
HANDLE hOutEvent, hAddEvent;
DWORD WINAPI thread(LPVOID)
{
int i;
for (i = 0; i < 10; i++)
{
++n;
if (i == 4)
{
SetEvent(hOutEvent);
WaitForSingleObject(hAddEvent, INFINITE);
}
}
return 0;
}

int main()
{

HANDLE hThread;
DWORD IDThread;
cout << "An initial value of n = " << n << endl;
// создаем события с автоматическим сбросом hOutEvent =
CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent == NULL)
return GetLastError();
hAddEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if
(hAddEvent == NULL)
return GetLastError();
62
// создаем поток счетчик thread
hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if
(hThread == NULL)
return GetLastError();
// ждем пока поток thread выполнит половину работы
WaitForSingleObject(hOutEvent, INFINITE);
// выводим значение переменной
cout << "An intermediate value of n = " << n << endl;
// разрешаем дальше работать потоку thread SetEvent(hAddEvent);
WaitForSingleObject(hThread, INFINITE);
cout << "A final value of n = " << n << endl;
CloseHandle(hThread);
CloseHandle(hOutEvent);
CloseHandle(hAddEvent);
return 0;
}
Для перевода любого события в сигнальное состояние используется
функция SetEvent, которая имеет следующий прототип:
BOOL SetEvent(
HANDLE hEvent // дескриптор события
);
При успешном завершении эта функция возвращает значение TRUE, в
случае неудачи - FALSE.
Для перевода любого события в несигнальное состояние
используется функция ResetEvent, которая имеет следующий прототип:
BOOL ResetEvent(
HANDLE hEvent // дескриптор события
);
При успешном завершении эта функция возвращает значение TRUE, в
случае неудачи - FALSE.
Для освобождения нескольких потоков, ждущих сигнального
состояния события с ручным сбросом, используется функция PulseEvent,
которая имеет следующий прототип:
BOOL PulseEvent(
HANDLE hEvent // дескриптор события
);
При вызове этой функции все потоки, ждущие события с дескриптором hEvent,
выводятся из состояния ожидания, а само событие сразу переходит в
несигнальное состояние. Если функция PulseEvent вызывается для события с
автоматическим сбросом, то из состояния ожидания выводится только один из
ожидающих потоков. Если нет потоков, ожидающих сигнального состояния
события из функции PulseEvent, то состояние этого события остается
несигнальным. Однако заметим, что на платформе Windows NT/2000 для
выполнения этой функции требуется, чтобы в дескрипторе события был
установлен режим доступа EVENT_MODIFY_STATE.
Ниже приведен пример программы, использующей для синхронизации
события как с ручным, так и автоматическим сбросом.

Листинг 8. Пример синхронизации потоков при помощи событий с


ручным сбросом
#include <windows.h>

63
#include <iostream>
using namespace std;
volatile int n,m;
HANDLE hOutEvent[2], hAddEvent;
DWORD WINAPI thread_1 (LPVOID)
{
int i;
for (i = 0; i < 10; i++)
{
++n;
if (i == 4)
{
SetEvent(hOutEvent[0]);
WaitForSingleObject(hAddEvent, INFINITE);
}
}
return 0;
}

DWORD CALLBACK thread_2(LPVOID)


{

int i;
for (i = 0; i < 10; i++)
{
++m;
if (i == 4)
{
S etEvent(hOutEvent [ 1 ]);
WaitForSingleObject(hAddEvent, INFINITE);
}
}
return 0;
}

int main()
{

HANDLE hThread_1, hThread_2;


DWORD IDThread_1, IDThread_2;
cout << "An initial values of n = " << n << ", m = " << m <<
endl;
// создаем события с автоматическим сбросом hOutEvent[0] =
CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent[0] == NULL)
return GetLastError();
hOutEvent[1] = CreateEvent(NULL, FALSE, FALSE, NULL); if
(hOutEvent[1] == NULL)
return GetLastError();
// создаем событие с ручным сбросом
hAddEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (hAddEvent == NULL)
return GetLastError();
// создаем потоки счетчики
hThread_1 = CreateThread(NULL, 0, thread_1, NULL, 0,
&IDThread_1); if (hThread_1 == NULL)
return GetLastError();
hThread_2 = CreateThread(NULL, 0, thread_2, NULL, 0,
&IDThread_2); if (hThread_2 == NULL)

64
return GetLastError();
// ждем пока потоки счетчики выполнят половину работы
WaitForMultipleObjects(2, hOutEvent, TRUE, INFINITE); cout << "An
intermediate values of n = " << n
<< ", m = " << m << endl;
// разрешаем потокам счетчикам продолжать работу
SetEvent(hAddEvent);
// ждем завершения потоков W aitForSingleObj ect(hThread_ 1,
INFINITE);
W aitForSingleObj ect(hThread_2, INFINITE);
cout << "A final values of n = " << n << ", m = " << m << endl;
CloseHandle(hThread_ 1);
CloseHandle(hThread_2);
CloseHandle(hOutEvent[0]);
CloseHandle(hOutEvent[ 1 ]);
CloseHandle(hAddEvent);
return 0;
}
Доступ к существующему событию можно открыть с помощью одной из
функций CreateEvent или OpenEvent. Если для этой цели используется функция
CreateEvent, то значения параметров bManualReset и bInitialState этой функции
игнорируются, так как они уже установлены другим потоком, а поток,
вызвавший эту функцию, получает полный доступ к событию с именем,
заданным параметром lpName. Теперь рассмотрим
функцию OpenEvent, которая используется в случае, если известно, что событие
с заданным именем уже существует. Эта функция имеет следующий прототип:
HANDLE OpenEvent(
DWORD dwDesiredAccess, // флаги доступа
BOOL bInheritHandle, // режим наследования
LPCTSTR lpName // имя события
);
Параметр dwDesiredAccess определяет доступ к событию, и может
быть равен любой логической комбинации следующих флагов:
E VENT_ALL_ACCES S
EVENT_MODIFY_STATE
SYNCHRONIZE
Флаг EVENT_ALL_ACCESS означает, что поток может выполнять над
событием любые действия. Флаг EVENT_MODIFY_STATE означает, что поток
может использовать функции SetEvent и ResetEvent для изменения состояния
события. Флаг SYNCHRONIZE означает, что поток может использовать
событие в функциях ожидания.
В завершение параграфа приведем пример синхронизации потоков,
выполняющихся в разных процессах, при помощи события с автоматическим
сбросом. В этом примере также используется функция OpenEvent для доступа к
уже существующему событию.

Листинг 9. Пример синхронизации потоков в разных процессах с


использованием именованного события
#include <windows.h>
#include <iostream>
using namespace std;
65
HANDLE hInEvent;
CHAR lpEventName[]="InEventName";
int main()
{
char c;
hInEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, lpEventName);
if (hInEvent == NULL)
{
cout << "Open event failed." << endl; cout << "Input any char
to exit." << endl; cin >> c;
return GetLastError();
}
cout << "Input any char: "; cin >> c;
// устанавливаем событие о вводе символа SetEvent(hInEvent);
// закрываем дескриптор события в текущем процессе
CloseHandle(hInEvent);
cout << "Now input any char to exit from the process: "; cin >>
c;
return 0;
}

Листинг 10. Пример синхронизации потоков в разных процессах // с


использованием именованного события
#include <windows.h>
#include <iostream>
using namespace std;
HANDLE hInEvent;
CHAR lpEventName[] = "InEventName";
int main()
{
DWORD dwWaitResult;
char szAppName[] = "C:\\ConsoleProcess.exe";
STARTUPINFO si;
PROCESS_INFORMATION pi;
// создаем событие, отмечающее ввод символа
hInEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName);
if (hInEvent == NULL)
return GetLastError();
// запускаем процесс, который ждет ввод символа ZeroMemory(&si,
sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO);
if (!CreateProcess(szAppName, NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
return 0;
// закрываем дескрипторы этого процесса
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// ждем оповещение о наступлении события от этого процесса
dwWaitResult = WaitForSingleObject(hInEvent, INFINITE); if
(dwWaitResult != WAIT_OBJECT_0) return dwWaitResult;
cout << "A symbol has got." << endl;
CloseHandle(hInEvent);
cout << "Press any key to exit: "; cin.get();
return 0;
}
Кратко опишем работу этих программ. Вторая из них запускает первую
66
программу, после чего ждет, пока первая программа не введет какой-нибудь
символ. После ввода символа обе программы заканчивают свою работу. Для
оповещения второй программы о вводе символа используется именованное
событие.

Задание на выполнение
1. Используя представленные в работе примеры программ, реализовать
приложения, демонстрирующие использование объектов синхронизации
мьютексы и события.
2. Разработать приложение для консольных процессов, характеристики
которых указаны в соответствующем варианте.
3. Для синхронизации консольных процессов использовать мьютексы и
события.
4. Результаты работы представить преподавателю в виде отчета и
продемонстрировать функционирующее приложение.

Варианты заданий
1. Написать программы для консольного процесса Boss (Резидент) и
консольных процессов Scout (Шпион). При реализации синхронизации
процессов использовать функции ожидания сигнального состояния объекта
только с равным нулю или бесконечности интервалом ожидания. Каждый
отдельный процесс открывать в отдельном консольном окне.
Для моделирования передачи сообщений ввести специальные события,
которые обозначают «точку» и «тире», конец сеанса.
Процесс Boss:
− запрашивает у пользователя количество процессов Scout, которые он
должен запустить;
− запускает заданное количество процессов Scout;
− принимает от каждого процесса Scout сообщение и выводит его на
консоль в одной строке. Принимать сообщение может только от одного
процесса, передача остальных сообщений от других процессов должна
блокироваться с помощью мьютекса;
− завершает свою работу.
Процесс Scout:
− запрашивает с консоли символы: «-», «.» (событие «тире», событие
«точка» ) и передает соответствующие события процессу Boss;
− завершает свою работу, когда будет введён символ, обозначающий
конец ввода сообщений.
2. Написать программы для консольного процесса Boss (Резидент) и
консольных процессов Scout (Шпион).
Для моделирования передачи сообщений ввести специальные события,
которые обозначают «1» , «2» и конец сеанса для процессов Scout
Процесс Boss:
− запрашивает у пользователя количество процессов Scout, которые он
должен запустить;
− запускает заданное количество процессов Scout;
67
− принимает от каждого процесса Scout сообщение и выводит его на
консоль в одной строке. Принимать сообщение может только от двух
процессов, передача остальных сообщений от других процессов должна
блокироваться с помощью мьютексов;
− завершает свою работу.
Процесс Scout:
− запрашивает с консоли сообщения, состоящее из «1» , «2», и передает
их (по одному) процессу Boss;
− завершает свою работу.
3. Написать программы для консольного процесса Boss и консольных
процессов Parent, Child. Для моделирования передачи сообщений ввести
специальные события, которые обозначают любые 4-е цифры и конец сеанса
для процессов Parent и Child
Процесс Boss:
− запрашивает у пользователя количество процессов Parent и количество
процессов Child, которые он должен запустить;
− запрашивает кол-во сообщений, отправленных Parent и Child
− запускает заданное количество процессов Parent, Child;
− отправляет сообщения для процессов Parent, Child Отправить сообщение
может только трём процессам из всех процессов Child и Parent, передача
остальных сообщений от других процессов должна блокироваться с
помощью мьютексов;
− завершает свою работу.
Процесс Parent:
− получает сообщение, от процесса Boss и выводит его на консоль;
− завершает свою работу.
Процесс Child:
− получает сообщение, от процесса Boss и выводит его на консоль;
завершает свою работу.
4. Написать программы для консольного процесса Boss (Резидент) и
консольных процессов Scout (Шпион).
Для моделирования передачи сообщений ввести специальные события,
которые обозначают любые 4-е цифры.
Процесс Boss:
− запрашивает у пользователя количество процессов Scout, которые он
должен запустить;
− запрашивает у пользователя пароль (3 цифры);
− запускает заданное количество процессов Scout;
− принимает от каждого процесса Scout сообщение и выводит его на
консоль в одной строке. Принимать сообщение может только от трёх
процессов, передача остальных сообщений от других процессов должна
блокироваться;
− если приходит сообщение, с цифрой не из пароля, то выводит на консоль
текст "ошибка";
− завершает свою работу.
Процесс Scout:
68
− запрашивает с консоли сообщение, состоящее из цифр, и передает их (по
одному) процессу Boss;
− завершает свою работу.
5. Написать программы для консольного процесса Boss и консольных
процессов Parent, Child. Для моделирования передачи сообщений ввести
специальные события, которые обозначают «А» , «В» и конец сеанса для
процессов Parent и Child.
Процесс Boss:
− запрашивает у пользователя количество процессов Parent и количество
процессов Child, которые он должен запустить;
− запускает заданное количество процессов Parent, Child;
− запрашивает кол-во сообщений, полученных от Parent или Child
− принимает от каждого процесса Parent, Child сообщение и выводит
сообщение и кто его отправил на консоль в одной строке. Принимать
сообщение может только от одного процесса Child и одного процесса
Parent, передача остальных сообщений от других процессов должна
блокироваться с помощью мьютексов;
− завершает свою работу.
Процесс Parent:
− запрашивает с консоли сообщения, состоящее из «А» и передает их (по
одному) процессу Boss;
− завершает свою работу.
Процесс Child:
− запрашивает с консоли сообщения, состоящее из «В» » и передает их (по
одному) процессу Boss;
− завершает свою работу.
6. Написать программы для консольного процесса Administrator и
консольных процессов Reader и Writer.
Для моделирования передачи сообщений ввести специальные события,
которые обозначают сообщение “A”, сообщение “В”, и конец сеанса для
процессов Reader и Writer.
Одновременно принимать и отправлять сообщения могут только два
процесса Writer и два процесса Reader, передача остальных сообщений от
других процессов должна блокироваться с помощью мьютексов;
Процесс Administrator:
− запрашивает у пользователя количество процессов Writer( Reader);
− запрашивает у пользователя количество отправленных (полученных)
сообщений для процессов Writer (Reader);
− запускает заданное количество процессов Reader и Writer;
− принимает от каждого процесса Writer сообщение и выводит на консоль,
затем отправляет его процессу Reader;
− принимает от каждого процесса Reader и Writer сообщение о
завершении сеанса и выводит его на консоль в одной строке;
− завершает свою работу.
Процесс Writer:
− запрашивает с консоли сообщения, состоящее из “A" , “В", и передает их
69
(по одному) процессу Administrator;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
Процесс Reader:
− принимает сообщение от процесса Administrator;
− выводит на консоль сообщение;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
7. Написать программы для консольного процесса Boss и консольных
процессов Parent, Child. Для моделирования передачи сообщений ввести
специальные события, которые обозначают «А» , «В», «С» , «D» и конец сеанса
для процессов Parent и Child.
Процесс Boss:
− запрашивает у пользователя количество процессов Parent и количество
процессов Child, которые он должен запустить;
− запускает заданное количество процессов Parent, Child;
− запрашивает количество сообщений, принятых от Parent или Child
− принимает от каждого процесса Parent, Child сообщение и выводит
сообщение и кто его отправил на консоль в одной строке. Принимать
сообщение может только от двух процессов Child и одного процесса
Parent, передача остальных сообщений от других процессов должна
блокироваться с помощью мьютексов;
− завершает свою работу.
Процесс Parent:
− запрашивает с консоли сообщения, состоящее из «А» , «В» и передает их
(по одному) процессу Boss;
− завершает свою работу.
Процесс Child:
− запрашивает с консоли сообщения, состоящее из «С», «D» и передает
их (по одному) процессу Boss;
− завершает свою работу.
8. Написать программы для консольного процесса Administrator и
консольных процессов Reader и Writer.
Для моделирования передачи сообщений ввести специальные события, которые
обозначают сообщение “A", сообщение “В", и конец сеанса для процессов
Reader и Writer.
Одновременно принимать и отправлять сообщения могут только один процесс
Writer и один процесс Reader, передача остальных сообщений от других
процессов должна блокироваться с помощью мьютексов.
Процесс Administrator:
− запрашивает у пользователя количество процессов Reader и Writer,
которые он должен запустить;
− запрашивает у пользователя количество отправленных сообщений для
процесса Writer и количество принятых сообщений для процесса
Reader(соответствие сообщений проверить и подкорректировать по
формуле);
70
− запускает заданное количество процессов Reader и Writer;
− принимает от каждого процесса Reader и Writer сообщение о
завершении сеанса и выводит его на консоль в одной строке.
− завершает свою работу.
Процесс Writer:
− запрашивает с консоли сообщения, и передает их (по одному) процессу
Reader;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
Процесс Reader:
− принимает сообщение от процесса Writer;
− выводит на консоль сообщение;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
9. Написать программы для консольного процесса Boss и консольных
процессов Employee. Для моделирования передачи сообщений ввести
специальные события, которые «0» , «1», «2», «3» и конец сеанса для процессов
Employee .
Процесс Boss:
− запрашивает у пользователя количество процессов Employee, которые
он должен запустить;
− запускает заданное количество процессов Employee;
− принимает от каждого процесса Employee сообщение и выводит его на
консоль в одной строке. Принимать сообщение может только от трёх
процессов, передача остальных сообщений от других процессов
должна блокироваться с помощью мьютексов;
− завершает свою работу.
Процесс Employee:
− запрашивает с консоли сообщения, состоящее из «0» , «1», «2», «3»,
конец сеанса работы и передает (по одному) его процессу Boss;
− завершает свою работу.
10. Написать программы для консольного процесса Administrator и
консольных процессов Reader и Writer.
Для моделирования передачи сообщений ввести специальные события,
которые обозначают сообщение “Л”, сообщение “Б”, и конец сеанса для
процессов Reader и Writer.
Одновременно принимать и отправлять сообщения могут только два
процесса Writer и два процесса Reader, передача остальных сообщений от
других процессов должна блокироваться с помощью мьютексов;
Процесс Administrator:
− запрашивает у пользователя количество процессов Reader и Writer,
которые он должен запустить;
− запрашивает у пользователя кол-во отправленных сообщений для
процесса Writer. Кол-во принятых сообщений для процесса Reader
вычислить. (соответствие сообщений проверить и подкорректировать по
формуле);
71
− запускает заданное количество процессов Reader и Writer;
− принимает от каждого процесса Reader и Writer сообщение о завершении
сеанса и выводит его на консоль в одной строке.
− завершает свою работу.
Процесс Writer:
− запрашивает с консоли сообщения, и передает их (по одному) процессу
Reader;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
Процесс Reader:
− принимает сообщение от процесса Writer;
− выводит на консоль сообщение;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
11. Написать программы для консольного процесса Administrator и
консольных процессов Reader и Writer.
Для моделирования передачи сообщений ввести специальные события, которые
обозначают сообщение “Л” , сообщение “Б”, и конец сеанса для процессов
Reader и Writer.
Одновременно принимать и отправлять сообщения могут только один процесс
Writer и два процесса Reader, передача остальных сообщений от других
процессов должна блокироваться с помощью мьютексов;
Процесс Administrator:
− запрашивает у пользователя количество процессов Reader и Writer,
которые он должен запустить;
− запрашивает у пользователя кол-во отправленных сообщений для
процесса Writer. Кол-во принятых сообщений для процесса Reader
вычислить. (соответствие сообщений проверить и подкорректировать
по формуле);
− запускает заданное количество процессов Reader и Writer;
− принимает от каждого процесса Reader и Writer сообщение о
завершении сеанса и выводит его на консоль в одной строке.
− завершает свою работу.
Процесс Writer:
− запрашивает с консоли сообщения, и передает их (по одному) процессу
Reader;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.
Процесс Reader:
− принимает сообщение от процесса Writer;
− выводит на консоль сообщение;
− передает сообщение о завершении сеанса процессу Administrator;
− завершает свою работу.

Контрольные вопросы
1. Поясните цели синхронизации процессов и потоков многозадачных ОС.
72
2. Что такое мьютексы (mutex) и чем они отличаются от критических
областей?
3. Какие функции и типы Windows API используются для получения
доступа к мьютексу, его захвата и освобождения?
4.Что такое события (event) в Windows API и для чего они могут
использоваться?
5. Какие существуют типы событий и чем они отличаются друг от друга?
6. Какими функциями Windows API осуществляется работа с событиями?
7. В каком случае событие, сбрасываемое вручную, всё-таки сбрасывается
автоматически?
8. Поясните общие свойства и различия при использовании событий и
мьютексов.

Лабораторная работа №6
Тема: Обмен данными по анонимному каналу с сервером

Цель работы:
1. Изучение механизмов межпроцессного обмена в ОС семейства Windows.
2. Изучить функции для работы с анонимными каналами.
3. В соответствии с заданным вариантом разработать приложение,
реализующее обмен данными между процессами с помощью анонимных
каналов.

Краткое теоретическое введение


1. Функции для работы с анонимными каналами
1.1. Создание анонимных каналов
Анонимные каналы создаются процессом сервером при помощи
функции CreatePipe, которая имеет следующий прототип:
BOOL CreatePipe (
PHANDLE hReadHandle, // дескриптор для чтения из
канала
PHANDLE hWriteHandle, // дескриптор для записи в
канал
LPSECURITY_ATTRIBUTES lpPipeAttributes, // атрибуты защиты
DWORD dwSize // размер буфера в байтах
);
При удачном завершении функция CreatePipe возвращает значение TRUE,
а в случае неудачи – FALSE. Отметим, что операционные системы Windows
автоматически определяют размер буфера и поэтому значение параметра
dwSize можно установить равным 0, тогда операционная система выберет
размер буфера по умолчанию.

1.2 Соединение клиентов с анонимным каналом


Так как анонимные каналы не имеют имени, то для соединения процесса-
клиента с таким каналом необходимо передать клиенту один из дескрипторов

73
анонимного канала. При этом передаваемый дескриптор должен быть
наследуемым. Наследование дескрипторов анонимного канала определяется
значением поля bInheritHandle в структуре типа SECURITY_ATTRIBUTES, на
которую указывает параметр lpPipeAttributes функции CreatePipe. В ранних
версиях Windows эта задача решалась путем создания соответственно
ненаследуемого или наследуемого дубликата исходного дескриптора,
используя функцию DuplicateHandle. После этого исходный дескриптор
закрывается. В операционной системе Windows 2000 эта задача может быть
также решена, используя функцию SetHandleInformation, которая изменяет
свойство наследования дескриптора.
Передача наследуемого дескриптора клиенту может выполняться одним из
следующих способов:
− через командную строку;
− через поля hStdInput, hStdOutput и hStdError структуры
STARTUPINFO;
− посредством сообщения WM_COPYDATA;
− через файл.
В данной работе будут использованы только первых два способа передачи
дескрипторов процессу-клиенту. Третьим способом можно пользоваться только
процессам с графическим интерфейсом (GUI).

1.3 Обмен данными по анонимному каналу


Для обмена данными по анонимному каналу в операционных системах
Windows используются те же функции, что для записи и чтения данных в файл.
Для записи данных в анонимный канал используется функция WriteFile, которая
имеет следующий прототип:
BOOL WriteFile(
HANDLE hAnonymousPipe, // дескриптор
анонимного канала
LPCVOID lpBuffer, // буфер данных
DWORD dwNumberOffiytesToWrite, // число байт для
записи
LPDWORD lpNumberOfBytesWritten, // число записанных
байт
LPOVERLAPPED lpOverlapped // асинхронный ввод
);
Функция WriteFile записывает в анонимный канал количество байт,
заданных параметром dwNumberOfBytesToWrite, из буфера данных, на который
указывает параметр lpBuffer. Дескриптор вывода этого анонимного канала
должен быть задан первым параметром функции WriteFile.
Для чтения данных из анонимного канала используется функция ReadFile,
которая имеет следующий прототип
BOOL ReadFile (
HANDLE hAnonymousPipe, // дескриптор
анонимного канала
LPCVOID lpBuffer, // буфер данных
DWORD dwNumberOffiytesToRead, // число байт для
записи

74
LPDWORD lpNumberOfBytesRead, // число записанных байт
LPOVERLAPPED lpOverlapped // асинхронный ввод
);
Функция ReadFile читает из анонимного канала количество байт, заданных
параметром dwNumberOfBytesToRead, в буфер данных, на который указывает
параметр lpBuffer. Дескриптор ввода этого ананимного канала должен быть
задан первым параметром функции ReadFile. Также как и в случае записи в
анонимный канал параметр lpOverlapped должен быть равен NULL.
Следует помнить, что обмен данными по анонимному каналу
осуществляется только в соответствии с назначением дескриптора этого канала.
Дескриптор для записи в анонимный канал должен быть параметром функции
WriteFile, а дескриптор для чтения из анонимного канала должен быть
параметром функции ReadFile. В этом и состоит смысл передачи данных по
анонимному каналу только в одном направлении. Один и тот же процесс
может, как писать данные в анонимный канал, так и читать данные из него,
должным образом используя дескрипторы этого анонимного канала.
После завершения обмена данными по анонимному каналу, потоки должны
закрыть дескрипторы записи и чтения анонимного канала, используя функцию
CloseHandle.

2. Примеры работы с анонимными каналами


Вначале рассмотрим простой пример, в котором процесс-сервер выполняет
следующие действия:
− создание анонимного канала;
− создание дочернего процесса;
− передача созданному дочернему процессу одного из дескрипторов
созданного анонимного канала, используя для этого командную строку.
В этом случае дочерний процесс будет клиентом анонимного канала. Для
определенности передадим клиенту дескриптор для записи в анонимный канал
и оставим серверу дескриптор для чтения. Сначала приведем программу
процесса-клиента анонимного канала.

Листинг 1. Пример процесса клиента анонимного канала.


// Клиент пишет в анонимный канал.
// Дескриптор анонимного канала передается клиенту через
командную строку.
#include <windows.h>
#include <conio.h>
int main(int argc, char *argv[])
{
HANDLE hWritePipe;
// преобразуем символьное представление дескриптора в число
hWritePipe = (HANDLE)atoi(argv[1]);
// ждем команды о начале записи в анонимный канал
_cputs("Press any key to start communication.\n");
_getch();
// пишем в анонимный канал
for (int i = 0; i < 10; i++)
{
75
DWORD dwBytesWritten;
if (!WriteFile(
hWritePipe,
&i,
sizeof(i),
&dwBytesWritten,
NULL))
{
_cputs("Write to file failed.\n");
_cputs("Press any key to finish.\n");
_getch();
return GetLastError();
}
_cprintf("The number %d is written to the pipe.\n", i);
Sleep(500); }
// закрываем дескриптор канала
CloseHandle(hWritePipe);
_cputs("The process finished writing to the pipe.\n");
_cputs("Press any key to exit.\n");
_getch();
return 0;
}

Теперь приведем программу процесса-сервера анонимного канала,


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

Листинг 2. Пример процесса сервера анонимного канала.


// Сервер читает из анонимного канала. // Дескриптор анонимного
канала передается // клинету через командную строку.
#include <windows.h> #include <conio.h>
int main() {
char lpszComLine[80]; // для командной строки
STARTUPINFO si;
PROCESS_INFORMATION pi;
HANDLE hWritePipe, hReadPipe, hInheritWritePipe;
// создаем анонимный канал if(!CreatePipe(
&hReadPipe, // дескриптор для чтения
&hWritePipe, // дескриптор для записи
NULL, // атрибуты защиты по умолчанию,
// в этом случае дескрипторы
// hReadPipe и hWritePipe ненаследуемые
0)) // размер буфера по умолчанию
{
_cputs("Create pipe failed.\n");
_cputs("Press any key to finish.\n");
_getch();
return GetLastError();
}
// делаем наследуемый дубликат дескриптора hWritePipe
if(!DuplicateHandle(
GetCurrentProcess(), // дескриптор текущего процесса
hWritePipe, // исходный дескриптор канала

76
GetCurrentProcess(), // дескриптор текущего процесса
&hInheritWritePipe, // новый дескриптор канала
0, // этот параметр игнорируется
TRUE, // новый декскриптор наследуемый
DUPLICATE_SAME_ACCESS ))// доступ не изменяем
{
_cputs("Duplicate handle failed.\n");
_cputs("Press any key to finish.\n");
_getch();
return GetLastError();
}
// закрываем ненужный дескриптор CloseHandle(hWritePipe);
// устанавливаем атрибуты нового процесса ZeroMemory(&si,
sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO);
// формируем командную строку wsprintf(lpszComLine,
"C:\\Client.exe %d", (int)hInheritWritePipe);
// запускаем новый консольный процесс if (!CreateProcess(
NULL, // имя процесса
lpszComLine, // командная строка
NULL, // атрибуты защиты процесса по умолчанию
NULL, // атрибуты защиты первичного потока по умолчанию
TRUE, // наследуемые дескрипторы текущего процесса
// наследуются новым процессом
CREATE_NEW_CONSOLE, // новая консоль
NULL, // используем среду окружения процесса предка
NULL, // текущий диск и каталог, как и в процессе предке
&si, // вид главного окна - по умолчанию
&pi // здесь будут дескрипторы и идентификаторы
// нового процесса и его первичного потока )) {
_cputs("Create process failed.\n"); _cputs("Press any key to
finish.\n"); _getch();
return GetLastError(); }
// закрываем дескрипторы нового процесса
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
// закрываем ненужный дескриптор канала
CloseHandle(hInheritWritePipe);
// читаем из анонимного канала for (int i = 0; i < 10; i++) {
int nData;
DWORD dwBytesRead;
if (!ReadFile(
hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) {
_cputs("Read from the pipe failed.\n"); _cputs("Press any key
to finish.\n"); _getch();
return GetLastError(); } _cprintf("The number %d is read from
the pipe.\n", nData); }
// закрываем дескриптор канала CloseHandle(hReadPipe);
_cputs("The process finished reading from the pipe.\n");
_cputs("Press any key to exit.\n");
_getch();
return 0; }
В следующих программах показывается, как организовать двусторонний
обмен данными по анонимному каналу между клиентом и сервером. Для этого
дескрипторы чтения и записи в анонимный канал используются как сервером,
так и клиентом этого анонимного канала. В этом примере также сначала
77
приведена программа процесса-клиента анонимного канала.

Листинг 3. Пример процесса клиента анонимного канала.


// Клиент сначала пишет в анонимный канал, а потом читает из
него.
// Дескриптор анонимного канала передается клиенту через
командную строку.
#include <windows.h> #include <conio.h>
int main(int argc, char *argv[]) {
HANDLE hWritePipe, hReadPipe;
HANDLE hEnableRead; // для синхронизации обмена данными
char lpszEnableRead[] = "EnableRead";
// открываем событие, разрешающее чтение hEnableRead =
OpenEvent(EVENT_ALL_ACCESS, FALSE, lpszEnableRead);
// преобразуем символьное представление дескрипторов в число
hWritePipe = (HANDLE)atoi(argv[1]); hReadPipe =
(HANDLE)atoi(argv[2]);
// ждем команды о начале записи в анонимный канал
_cputs("Press any key to start communication.\n"); _getch();
// пишем в анонимный канал for (int i = 0; i < 10; i++) {
DWORD dwBytesWritten; if (!WriteFile(
hWritePipe, &i,
sizeof(i),
&dwBytesWritten, NULL)) {
_cputs("Write to file failed.\n"); _cputs("Press any key to
finish.\n"); _getch();
return GetLastError(); } _cprintf("The number %d is written to
the pipe.\n", i); } _cputs("The process finished writing to the
pipe.\n");
// ждем разрешения на чтение WaitForSingleObject(hEnableRead,
INFINITE); // читаем ответ из анонимного канала for (int j = 0; j
< 10; j++) {
int nData;
DWORD dwBytesRead;
if (!ReadFile(
hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) {
_cputs("Read from the pipe failed.\n"); _cputs("Press any key
to finish.\n"); _getch();
return GetLastError(); } _cprintf("The number %d is read from
the pipe.\n", nData); }
_cputs("The process finished reading from the pipe.\n");
_cputs("Press any key to exit.\n"); _getch();
// закрываем дескрипторы канала CloseHandle(hWritePipe);
CloseHandle(hReadPipe); CloseHandle(hEnableRead);
return 0; }

Теперь приведем текст программы процесса-сервера анонимного канала,


который запускает клиента и передает ему дескрипторы анонимного канала
через командную строку.

Листинг 4. Пример процесса сервера анонимного канала.


// Сервер сначала читает из анонимного канала, а затем пишет в
него.
78
// Дескриптор анонимного канала передается клиенту через
командную строку.
#include <windows.h> #include <conio.h>
int main() {
char lpszComLine[80]; // для командной строки
HANDLE hEnableRead; // для синхронизации обмена данными
char lpszEnableRead[] = "EnableRead";
STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe,
hReadPipe; SECURITY_ATTRIBUTES sa;
// создаем событие для синхронизации обмена данными hEnableRead
= CreateEvent(NULL, FALSE, FALSE, lpszEnableRead);
// устанавливает атрибуты защиты канала
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL; // защита по умолчанию
sa.bInheritHandle = TRUE; // дескрипторы наследуемые
// создаем анонимный канал if(!CreatePipe(
&hReadPipe, // дескриптор для чтения
&hWritePipe, // дескриптор для записи
&sa, // атрибуты защиты по умолчанию,
// дескрипторы наследуемые
0)) // размер буфера по умолчанию
{
_cputs("Create pipe failed.\n"); _cputs("Press any key to
finish.\n"); _getch();
return GetLastError(); }
// устанавливаем атрибуты нового процесса ZeroMemory(&si,
sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO);
// формируем командеую строку wsprintf(lpszComLine,
"C:\\Client.exe %d %d",
(int)hWritePipe, (int)hReadPipe); // запускаем новый консольный
процесс if (!CreateProcess(
NULL, // имя процесса
lpszComLine, // командная строка
NULL, // атрибуты защиты процесса по умолчанию
NULL, // атрибуты защиты первичного потока по умолчанию
TRUE, // наследуемые дескрипторы текущего процесса
// наследуются новым процессом
CREATE_NEW_CONSOLE, // новая консоль
NULL, // используем среду окружения процесса предка
NULL, // текущий диск и каталог, как и в процессе предке
&si, // вид главного окна - по умолчанию
&pi // здесь будут дескрипторы и идентификаторы
// нового процесса и его первичного потока )) {
_cputs("Create process failed.\n"); _cputs("Press any key to
finish.\n"); _getch();
return GetLastError(); }
// закрываем дескрипторы нового процесса
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
// читаем из анонимного канала for (int i = 0; i < 10; i++) {
int nData;
DWORD dwBytesRead;
if (!ReadFile(
hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) {
_cputs("Read from the pipe failed.\n"); _cputs("Press any key

79
to finish.\n"); _getch();
return GetLastError(); } _cprintf("The number %d is read from
the pipe.\n", nData); } _cputs("The process finished reading from
the pipe.\n");
// даем сигнал на разрешение чтения клиентом
SetEvent(hEnableRead);
// пишем ответ в анонимный канал for (int j = 10; j < 20; j++)
{
DWORD dwBytesWritten; if (!WriteFile(
hWritePipe, &j,
sizeof(j),
&dwBytesWritten, NULL)) {
_cputs("Write to file failed.\n"); _cputs("Press any key to
finish.\n"); _getch();
return GetLastError(); } _cprintf("The number %d is written to
the pipe.\n", j); }
// закрываем дескрипторы канала CloseHandle(hReadPipe);
CloseHandle(hWritePipe); CloseHandle(hEnableRead);
_cputs("The process finished writing to the pipe.\n");
_cputs("Press any key to exit.\n");
_getch();
return 0; }

Отметим в последнем примере следующий момент. Для организации


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

3. Перенаправление стандартного ввода-вывода


Анонимные каналы часто используются для перенаправления
стандартного ввода-вывода. Чтобы подробнее разобраться с этим вопросом,
сначала кратко рассмотрим стандартные средства ввода-вывода, используемые
в языке С++. Компилятор языка C++ фирмы Micros