Открыть Электронные книги
Категории
Открыть Аудиокниги
Категории
Открыть Журналы
Категории
Открыть Документы
Категории
Г.С.Алтыбаев, А.Н.Иманбекова
Учебно-методическое пособие
Рецензенты:
Аязбаев Т.Л. – к.ф.-м.н., доцент кафедры «Математика и
вычислительная техника» Таразского инновационно-
гуманитарного университета;
Мамаев Ш.М. – д.ф.-м.н., заведующий кафедрой «Математика и
вычислительная техника» Таразского инновационно-
гуманитарного университета;
Сембина Г.К. – к.т.н., доцент, заведующая кафедрой
«Информационные системы» Таразского
государственного университета им. М.Х. Дулати.
ВВЕДЕНИЕ 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
ВВЕДЕНИЕ
5
Лабораторная работа №1
Создание окон с помощью Win32 API функций
Цель работы:
1. Изучение принципов организации и функционирования стандартных
приложений Windows.
2. Изучение структуры приложений, использующих функции Win32API.
3. Создание каркаса оконного приложения Win32 и исследование его
структуры.
Головная функция
WinMain()
{
...
RegisterClass(&wc);
...
CreateWindow();
...
ShowWindow();
OS
... Windows Оконная
Цикл обработки сообщений функция
while();
{ WndProc()
{
} ...
return 0; }
}
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-приложения.
8
}
return 0;
}
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-приложения будет закончено.
15
Порядок выполнения работы
Рассмотрим сначала, как можно "вручную" создать минимальное
приложение Win32. Загрузив Visual Studio 2010, выполним команду File | New |
Project... и выберем тип проекта — Win32 Project. В раскрывающемся списке
Location выберем путь к рабочей папке, а в поле Name имя проекта (рисунок
3). В следующем диалоговом окне, приведенном на рисунке 4, нажимаем
кнопку Next, а в окне опций проекта (рисунок 5) выберем флажок Empty
project (Пустой проект) и нажмем кнопку Finish — получим пустой проект, в
котором нет ни одного файла.
16
Рисунок 5. Окно опций проекта
17
Программа не делает ничего полезного, поэтому, запустив ее на
выполнение кнопкой ► (Start Debugging), мы получим изображенное на
рисунке 8 пустое окно, имеющее заголовок и набор стандартных кнопок.
Контрольные вопросы
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, реализующего функции указанные в
варианте.
22
return 0;
}
23
процесса.
Приведем программу, которая демонстрируют работу функции
TerminateProcess. Для этого сначала создадим бесконечный процесс-счетчик,
который назовем ConsoleProcess.exe и расположим на диске C.
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, реализующую указанные в варианте
функции.
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. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью критических секций.
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;
}
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;
}
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
Приведем пример программы, в которой считающий семафор используется
для синхронизации работы потоков. Для этого сначала рассмотрим не
синхронизированный вариант этой программы.
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. В соответствии с заданным вариантом разработать приложение,
реализующее синхронизацию потоков с помощью
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.
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;
}
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;
}
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.
Покажем пример использования мьютекса для синхронизации потоков из
разных процессов. Для этого сначала рассмотрим пример не
синхронизированных потоков.
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;
}
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.
Ниже приведена программа, в которой безымянные события с
автоматическим сбросом используются для синхронизации работы потоков,
выполняющихся в одном процессе.
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.
Ниже приведен пример программы, использующей для синхронизации
события как с ручным, так и автоматическим сбросом.
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;
}
int i;
for (i = 0; i < 10; i++)
{
++m;
if (i == 4)
{
S etEvent(hOutEvent [ 1 ]);
WaitForSingleObject(hAddEvent, INFINITE);
}
}
return 0;
}
int main()
{
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 для доступа к
уже существующему событию.
Задание на выполнение
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. В соответствии с заданным вариантом разработать приложение,
реализующее обмен данными между процессами с помощью анонимных
каналов.
73
анонимного канала. При этом передаваемый дескриптор должен быть
наследуемым. Наследование дескрипторов анонимного канала определяется
значением поля bInheritHandle в структуре типа SECURITY_ATTRIBUTES, на
которую указывает параметр lpPipeAttributes функции CreatePipe. В ранних
версиях Windows эта задача решалась путем создания соответственно
ненаследуемого или наследуемого дубликата исходного дескриптора,
используя функцию DuplicateHandle. После этого исходный дескриптор
закрывается. В операционной системе Windows 2000 эта задача может быть
также решена, используя функцию SetHandleInformation, которая изменяет
свойство наследования дескриптора.
Передача наследуемого дескриптора клиенту может выполняться одним из
следующих способов:
− через командную строку;
− через поля hStdInput, hStdOutput и hStdError структуры
STARTUPINFO;
− посредством сообщения WM_COPYDATA;
− через файл.
В данной работе будут использованы только первых два способа передачи
дескрипторов процессу-клиенту. Третьим способом можно пользоваться только
процессам с графическим интерфейсом (GUI).
74
LPDWORD lpNumberOfBytesRead, // число записанных байт
LPOVERLAPPED lpOverlapped // асинхронный ввод
);
Функция ReadFile читает из анонимного канала количество байт, заданных
параметром dwNumberOfBytesToRead, в буфер данных, на который указывает
параметр lpBuffer. Дескриптор ввода этого ананимного канала должен быть
задан первым параметром функции ReadFile. Также как и в случае записи в
анонимный канал параметр lpOverlapped должен быть равен NULL.
Следует помнить, что обмен данными по анонимному каналу
осуществляется только в соответствии с назначением дескриптора этого канала.
Дескриптор для записи в анонимный канал должен быть параметром функции
WriteFile, а дескриптор для чтения из анонимного канала должен быть
параметром функции ReadFile. В этом и состоит смысл передачи данных по
анонимному каналу только в одном направлении. Один и тот же процесс
может, как писать данные в анонимный канал, так и читать данные из него,
должным образом используя дескрипторы этого анонимного канала.
После завершения обмена данными по анонимному каналу, потоки должны
закрыть дескрипторы записи и чтения анонимного канала, используя функцию
CloseHandle.
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
приведена программа процесса-клиента анонимного канала.
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; }