Форма отчета по лабораторной работе Отчет должен содержать: титульный лист, цель работы, условие задачи, текст программы с комментариями, скриншот окна с геометрическими фигурами, выводы по работе.
Вопросы для самоконтроля
1. Абстрактный класс.
2. Чистая виртуальная функция.
3. Использование указателей на абстрактные классы.
Лабораторная работа № 6. Изучение потоковой многозадачности
Цель и задачи работы, требования к результатам ее выполнения
Цель работы состоит в овладении навыками разработки программ на языке Си++, использующих возможности потоковой многозадачности. Для достижения цели необходимо выполнить следующие задачи:
- изучить необходимые учебные материалы, посвященные потоковой многозадачности в языке Си++ [4, 5];
- разработать программу на языке Си++ для решения заданного варианта задания;
- отладить программы;
- представить скриншот окна с результатами работы программы;
- подготовить отчет по лабораторной работе.
Краткая характеристика объекта изучения
Понятие многозадачность в Windows
В Windows существует два вида многозадачности:
- многозадачность, основанная на процессах;
- многозадачность, основанная на потоках (thread).
Процесс можно определить как копию (экземпляр) выполняющейся программы. В данном случае копия – понятие статическое. Т.е. процесс в Windows – это объект, который не выполняется, а просто «владеет» выделенным ему адресным пространством, другими словами, процесс – структура в памяти [4].
В адресном пространстве процесса находятся не только код и данные, но и потоки (thread) – выполняющиеся объекты. При запуске процесса автоматически запускается поток (он называется главным). Главный поток может запускать другие «дочерние» потоки.
Потоки могут работать параллельно (одновременно) друг с другом в многопроцессорных системах (в однопроцессорных системах работают «как бы параллельно» за счет временного разделения) с учетом их приоритетов и имеют доступ к ресурсам процесса (приложения).
Понятие потока (thread) как части процесса не следует путать с понятием потока ввода-вывода (stream).
Создание потока с помощью API – функций
Основные функции для работы с потоками имеют следующие заголовки (назначение параметров отмечено в виде комментариев).
Создать поток:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // дескриптор защиты
SIZE_T dwStackSize, // начальный размер стека (0 – как у родительского потока)
LPTHREAD_START_ROUTINE lpStartAddress, // функция потока
LPVOID lpParameter, // параметр потока
DWORD dwCreationFlags, // опции создания
LPDWORD lpThreadId // идентификатор потока
);
Потоковая функция, указатель на которую передается в качестве третьего параметра (lpStartAddress) функции CreateThread, вызывается при старте потока и должна иметь следующий заголовок (имя может быть любым):
DWORD WINAPI ThreadFun(LPVOID param);
В качестве параметра в функцию передается указатель (можно передавать через указатель любые необходимые данные), значение этого указателя определяется в четвертом параметра (lpParameter)функции CreateThread.
Приостановить поток:
DWORD SuspendThread(
HANDLE hThread // дескриптор (хэндл) потока
);
Возобновить приостановленный поток:
DWORD ResumeThread(
HANDLE hThread // дескриптор (хэндл) потока
);
Установить приоритет потока:
BOOL SetThreadPriority(
HANDLE hThread, // дескриптор потока
int nPriority // уровень приоритета потока
);
Возможные значения уровней приоритета (целые константы определены в стандартном заголовочном файле):
THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_ERROR_RETURN THREAD_PRIORITY_TIME_CRITICAL THREAD_PRIORITY_IDLE
Нормальное завершение потока – это естественный выход из потоковой функции. Также поток можно завершить из вне с помощью вызова функции:
BOOL TerminateThread(
HANDLE hThread, // дескриптор потока
DWORD dwExitCode // код завершения для потока
);
Возможностью завершения потока из вне следует пользоваться осторожно, так как, если поток работает с некоторыми ресурсами (файлами, сетевыми соединениями и др.), то эти ресурсы не будут автоматически освобождены.
Синхронизация потоков
Проблема синхронизации возникает, когда 2 или более потока пытаются «одновременно» получить доступ к одним и тем же данным (или одному объекту).
Объекты синхронизации и их назначение представлены в таблице 2.
Примеры использования объектов синхронизации:
Взаимное исключение.
HANDLE HMutex; // Объявляется глобально, и все потоки имеют доступ
HMutex=CreateMutex( // Создание взаимного исключения
NULL, // атрибуты прав доступа по умолчанию
FALSE, // Взаимное исключение изначально свободно
NULL); // не присваивается имя взаимному исключению
Работа с синхронизированным объектом:
WaitForSingleObject(HMutex, // Занять взаимное исключение
Таблица 2 – Объекты синхронизации и их назначение
Синхронизирующий объект
| Назначение
| Используемые функции Win32 API
| Взаимное исключение
| Запрет доступа более чем одному потоку к общим ресурсам
| CreateMutex
WaitForSingleObject
WaitForMultipleObjects
ReleaseMutex
CloseHandle
| Критическая секция
| Запрет доступа более чем одному потоку к одному фрагменту кода
| InitializeCriticalSection
EnterCriticalSection
LeaveCriticalSection
DeleteCriticalSection
| Семафор
| Ограничение числа потоков, имеющих одновременный доступ к общим ресурсам
| CreateSemaphore
WaitForSingleObject
WaitForMultipleObjects
ReleaseSemaphore
CloseHandle
| Событие
| Позволяет потоку передавать сигналы другим потокам
| CreateEvent
SetEvent
PulseEvent
ResetEvent
WaitForSingleObject
WaitForMultipleObjects
CloseHandle
|
INFINITE); // ждите сколько нужно
Count++; // Работа с синхронизированным объектом
ReleaseMutex(HMutex); // Освободить взаимное исключение
Удалить взаимное исключение:
CloseHandle(HMutex);
Рассмотрим функции WaitForSingleObject и WaitForMultipleObjects. WaitForSingleObject имеет заголовок:
DWORD WaitForSingleObject( HANDLE hObject, DWORD dwMilliseconds);
Когда поток вызывает эту функцию, первый параметр hObject идентифицирует объект ядра (объекты «Взаимное исключение», «Семафор» и «Событие» являются объектами ядра Windows [4]), поддерживающий состояния «свободен- занят». Второй параметр dwMilliseconds указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта, значение INFINITE означает ждать без ограничения времени. При выходе из функции она возвращает значение, которое можно использовать:
WAIT_OBJECT_0 – объект свободен;
WAIT_TIMEOUT – заданное время ожидания (таймаут) истекло;
WAIT_ FAILED – неверное значение параметра.
Функция WaitForMultipleObjects аналогична WaitForSingleObject, но она позволяет ждать освобождения сразу нескольких объектов или какого-то одного из них. Заголовок функции:
DWORD WaitForMultipleObjects( DWOHD dwCount, CONST HANDLE* phObjects, BOOL fWaitAll, DWORD dwMilliseconds);
Параметр dwCount определяет число ожидаемых объектов ядра, его значение в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObject – указатель на массив объектов ядра.
WaitForMultipleObjects приостанавливает поток и заставляет его ждать освобождения либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll это определяет, если он равен TRUE, функция не даст потоку возобновить свою работу, пока не освободятся все объекты, если FALSE, то достаточно освободиться хотя бы одному объекту.
Параметр dwMilliseconds идентичен одноименному параметру функции WaitForSingleObject.
2. Критическая секция – это фрагмент программы, защищенный от одновременного выполнения несколькими потоками. Критическую секцию в данный момент может выполнять только один поток.
InitializeCriticalSection – данная функция создает объект под названием критическая секция. Параметры функции:
указатель на структуру CRITICAL_SECTION.
Поля данной структуры используются только внутренними процедурами, их смысл безразличен.
EnterCriticalSection – войти в критическую секцию. После выполнения этой функции данный поток становится владельцем данной секции. Следующий поток, вызвав данную функцию, будет находиться в состоянии ожидания. Параметр функции такой же, что и в предыдущей функции.
LeaveCriticalSection – покинуть критическую секцию. После этого второй поток, который был остановлен функцией EnterCriticalSection, станет владельцем критической секции. Параметр функции LeaveCriticalSection такой же, как и у предыдущих функций.
DeleteCriticalSection – удалить объект "критическая секция". Параметр аналогичен предыдущим.
Семафор
Работает по аналогии с взаимным исключением, только доступ к объекту может получить не один поток, а их число определяется параметром MaximumCount (при занятии потоком семафора текущее значение счетчика уменьшается на 1, семафор полностью занят, если значение счетчика равно 0)
HANDLE CreateSemaphore( // Создать семафор
LPSECURITY_ATTRIBUTES lpAttr, // Атрибуты доступа
LONG InitialCount, // Начальное значение семафора
LONG MaximumCount, // Макс. значение
LPCTSTR lpName ); // Имя семафора
Возможная схема использования:
WaitForSingleObject(HSem, // Занять семафор
INFINITE); // ждите сколько нужно
Count++; // Работа с синхронизированным объектом
ReleaseSemaphore(HSem, 1, 0); // Освободить одно место семафора
Событие
Событие используется для того, чтобы один поток послал другому потоку, ожидающему наступление этого события, что событие произошло. Событием можно назначить любую точку в алгоритме программы, например, некоторая подзадача решена, т.е. получены необходимые результаты, являющиеся исходными данными для ожидающего события потока.
HANDLE hEvent;
hEvent=CreateEvent(0, // Создать объект - событие
false, // TRUE событие со сбросом вручную FALSE — событие с автосбросом
false, // свободное (TRUE) занятое (FALSE).
0);
WaitForSingleObject(hEvent,
INFINITE); // Программа далее не выполняется
Когда событие произошло необходимо вызвать в другом потоке функцию
SetEvent(hEvent);
Синхронизация процессов
С помощью объектов синхронизации можно организовать синхронизацию различных процессов. Например, доступ к объекту синхронизации, созданному в одном процессе (приложении), можно получить в другом приложении с помощью вызова функции OpenОбъект (Вместо термина объект идет имя объекта синхронизации). Например, для объекта «Событие» вызов имеет вид:
HANDLE OpenEvent(
DWORD dwDesiredAccess, // флаги доступа
BOOL bInheritHandle, // режим наследования
LPCTSTR lpName // имя события
);
Возможные значения флагов доступа:
- EVENT_ALL_ACCESS
- EVENT_MODIFY_STATE
- SYNCHRONIZE
6.2.6. Создание потока в Си++ с помощью стандартной библиотеки C++
Поддержка многопоточности в языке Си++ определена стандартом 2011 г. [5], до этого приходилось использовать платформенно-зависимые средства, как представлено выше. В продуктах компании Microsoft можно пользоваться стандартными средствами библиотеки Си++ для создания потоков, начиная с версии Microsoft Visual C++ 2011. Для создания потока необходимо использовать класс thread [5], входящий в пространство имен std. Ниже представлен листинг программы с комментариями. Программа создана как консольное приложение. В программе в главном потоке (функция main) создаются два дочерних потока, в потоковую функцию в качестве параметра передается строка текста (объект класса string), это строка печатается 10 раз.
// stdafx.h: включаемый файл для стандартных системных включаемых файлов
// или включаемых файлов для конкретного проекта, которые часто используются, но
// не часто изменяются
//
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
// TODO: Установите здесь ссылки на дополнительные заголовки, требующиеся для программы
#include <string>
#include <iostream>
#include <thread>
using namespace std;
// Thread1.cpp: определяет точку входа для консольного приложения.
//
#include "stdafx.h"
void myfun(string str) // Потоковая функция может иметь параметры при необходимости
{
for (int i = 0; i < 10; i++) // 10 раз печатаем строку
cout << endl << str.data();
}
int _tmain(int argc, _TCHAR* argv[])
{
thread th1(myfun, "Java"), th2(myfun, "C++");
// Создаем 2 объекта, первый параметр конструктора- указатель на потоковую функцию,
// следующие параметры передаются в потоковую функцию при необходимости
th1.join(); // Ждем завершение дочернего потока
th2.join(); // Ждем завершение дочернего потока
system("pause"); // Останавливаем программу до нажатия любой клавиши
return 0;
}
Результаты работы программы представлены на рисунке 4.
Рисунок 4 – Результаты работы многопоточной программы
На рисунке 4 в некоторых случаях виден беспорядочный вывод (каждая строка должна печататься с новой строчки, это правило иногда нарушено), что связано с отсутствием синхронизации (два потока работают одновременно с одним объектом cout).
Для синхронизации можно использовать объект класса mutex [5]. Ниже представлена программа с синхронизацией при печати.
// stdafx.h: включаемый файл для стандартных системных включаемых файлов
// или включаемых файлов для конкретного проекта, которые часто используются, но
// не часто изменяются
//
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
// TODO: Установите здесь ссылки на дополнительные заголовки, требующиеся для программы
#include <string>
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// Thread1.cpp: определяет точку входа для консольного приложения.
//
#include "stdafx.h"
mutex mut; // Создаем объект для синхронизации
void myfun(string str) // Потоковая функция может иметь параметры при необходимости
{
for (int i = 0; i < 10; i++) // 10 раз печатаем строку
{
mut.lock(); // Блокируем объект
cout << endl << str.data();
mut.unlock(); // Снимаем блокировку
}
}
int _tmain(int argc, _TCHAR* argv[])
{
thread th1(myfun, "Java"), th2(myfun, "C++");
// Создаем 2 объекта, первый параметр конструктора- указатель на потоковую функцию,
// следующие параметры передаются в потоковую функцию при необходимости
th1.join(); // Ждем завершение дочернего потока
th2.join(); // Ждем завершение дочернего потока
system("pause"); // Останавливаем программу до нажатия любой клавиши
return 0;
}
Результаты работы программы с синхронизацией представлены на рисунке 5.
Рисунок 5 – Результаты работы многопоточной программы с синхронизацией
|