Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ОС(СПО) Даниленко.pdf
Скачиваний:
69
Добавлен:
13.05.2015
Размер:
816.02 Кб
Скачать

3. Многопоточность в Windows

Цель работы: изучить функции и структуры данных Windows API, используемые для управления потоками, исследовать базовые механизмы взаимодействия потоков, ознакомиться с принципами управления приоритетами потоков.

3.1. Общие сведения

Понятие потока выполнения. Назначение потоков. Прило-

жение в системе Microsoft Windows состоит из одного и более процессов. ОС Windows распределяет все ресурсы, необходимые для выполнения программ, за исключением процессорного времени, между процессами. Каждый процесс имеет свое собственное адресное пространство, пользовательский контекст и системный контекст (например, таблицу объектов ядра), базовый приоритет, переменные окружения и т.п. Каждый процесс включает хотя бы один поток исполнения (thread of execution) – стартовый, но по мере необходимости могут создаваться и дополнительные потоки. Именно между потоками

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

ипеременные окружения.

Многопоточные приложения используются в следующих случа-

ях:

для управления вводом от нескольких окон;

для управления вводом от нескольких периферийных устройств или средств взаимодействия;

для эффективного и гибкого использования системы приоритетов;

для повышения интерактивности взаимодействия приложения и пользователя;

для обработки данных в «фоновом» режиме.

Использование одного многопоточного процесса зачастую эффективнее, чем применение многопроцессной реализации по следующим причинам:

переключение контекста между потоками происходит быстрее, чем между процессами, так как контекст процесса «больше», чем контекст потока;

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

потоки могут совместно использовать таблицу дескрипторов объектов ядра (например, файлов, каналов и т.п.).

Для создания потока (базовый поток создается при создании

процесса) используется системный вызов CreateThread (для создания потока в контексте другого процесса используется CreateRemoteThread). Объявление потока производится аналогично объявлению функции или процедуры, причем объявляемая функция может содержать формальный параметр – нетипизированный указатель, который может быть соответствующим образом разыменован и использован при реализации потока. Объявление функциипотока должно обязательно сопровождаться модификатором WINAPI, который скрывает за собой модификатор __stdcall (строго говоря, то что скрывается за макроопределением WINAPI зависит от аппаратной платформы и версии операционной системы).

Пример объявления функции-потока:

DWORD WINAPI ThreadFunc(LPVOID lpParam) { printf("ThreadFunc: Parameter = %d\n",

*(int*)lpParam ); getch();

return 0;

}

Согласование взаимодействия потоков может осуществляться с помощью функций вида «WaitFor…». Механизм согласования (синхронизации) в этом случае основан на том, что объекты синхронизации – как специализированные (события, мьютексы и т.п.), так и неспециализированные (файлы, процессы, потоки) – могут находиться в сигнальном и несигнальном состояниях. В частности, считается, что поток находиться в несигнальном состоянии, если он выполняется, и поток переходит в сигнальное состояние в тот момент, когда он завершается. Например, если необходимо в течении 2 секунд ожидать завершения потока, дескриптор которого hThread, то можно использовать следующую конструкцию:

WaitForSingleObject(hThread, 2000);

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

Windows API предоставляет набор функций, позволяющих управлять приоритетами потоков:

GetThreadPriority – возвращает значение приоритета потока;

SetThreadPriority – назначает значение приоритета потока;

GetProcessPriorityBoost – позволяет узнать, допускается ли динамическое изменение приоритета потока;

SetProcessPriorityBoost – включает или отключает режим

динамического изменения приоритета потока.

Как и для работы с процессами, существуют аналогичные функции, управляющие потоками, например GetThreadTimes,

TerminateThread и т.п.

При выполнении работы следует обратить внимание на аппаратное обеспечение вычислительной системы. При необходимости можно «заставить» потоки выполняться на одном ядре (процессоре)

используя функцию SetThreadAffinityMask или SetProcessAffinityMask.

3.2. Задание

1.Разработать и отладить программу, выполняющую следующие функции:

1.1.заполнение некоторого массива1 случайными числами;

1.2.создание в основном потоке двух дополнительных потоков, выполняющих сортировку массива двумя различными методами (см. приложение Б) и сохраняющих результаты в отдельных массивах;

1.3.основной поток ожидает завершения сортировки, сравнивает

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

2.Используя реализацию согласно п. 1, создать программу с учетом следующих требований:

2.1.после создания дополнительных потоков, изменить их базовые приоритеты в максимально большую сторону для одного и в меньшую для другого;

2.2.сравнить полученные временные характеристики выполнения потоков с результатами, полученными согласно п. 1.3;

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

2Для получения наглядных результатов, помимо значений возвращае-

мых функцией GetThreadTimes, необходимо находить время существования потока.

2.3.выполнить п. 2.1 и 2.2 поменяв более приоритетный и менее приоритетный потоки местами.

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

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

 

 

Приоритеты

Приоритет

Приоритет

 

 

равны

1-го больше

2-го больше

 

 

 

 

 

 

 

 

 

 

1-ый

2-ой

1-ый

2-ой

1-ый

2-ой

 

 

поток

поток

поток

поток

поток

поток

 

создания

 

 

 

 

 

 

Время

 

 

 

 

 

 

 

завершения

 

 

 

 

 

 

 

 

 

 

 

 

 

 

существования

 

 

 

 

 

 

 

 

 

 

 

 

 

 

работы

в пользователь-

 

 

 

 

 

 

ском режиме

 

 

 

 

 

 

в режиме ядра

 

 

 

 

 

 

Время

 

 

 

 

 

 

 

 

 

 

 

 

 

суммарное

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3.3. Контрольные вопросы

1.Дайте определения понятиям «процесс» и «поток». В чем сходство и отличие между процессом и потоком?

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

3.Что означает модификатор __stdcall?

4.Каким образом можно передать данные в поток?

5.Приоритеты потоков в Windows являются статическими или динамическими?

6.Зависит ли приоритет потока от приоритета процесса, в рамках которого он создается?

7.Каков механизм планирования потоков в Windows?

8.От каких факторов зависит время существования потока, длительность его работы в режиме пользователя и в режиме ядра?

4. Взаимодействие процессов в Windows

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

4.1. Общие сведения

Объекты ядра и их совместное использование. Функциони-

рование ОС Windows опирается на манипулирование объектами, имеющими определенное назначение и выполняющими специфические функции. К таким объектам можно отнести объекты процес-

сов/потоков (process/thread objects), объекты файлов (file objects),

объекты анонимных и именованных каналов (anonymous pipe objects, named pipe objects) и др. Эти объекты, называемые объектами ядра, представляют собой определенные структуры данных, расположенные в памяти, выделенной операционной системой. Структуры данных содержат информацию о фактических сущностях операционной системы, которые они представляют. Некоторые элементы этих структур являются общими – дескрипторы защиты, счетчик числа пользователей и т.п., другие – специфичны для каждого типа объектов. Структуры объектов ядра доступны только ядру операционной системы, поэтому пользовательское приложение не имеет непосредственного доступа к этим структурам, но может манипулировать ими посредством системных вызовов.

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

Механизм получения совместного доступа к объектам может быть реализован тремя способами. Первый способ использует насле-

1 Непосредственная манипуляция объектами осуществляется с помощью дескрипторов (handles) объектов.

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

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

DuplicateHandle.

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

Перенаправление стандартных потоков ввода-вывода и использование анонимных каналов. При создании стандартного кон-

сольного процесса, он автоматически получает для использования три дескриптора стандартных потоков ввода-вывода: дескриптор стандартного потока ввода STDIN (дескриптор буфера ввода консоли), дескриптор потока вывода STDOUT (дескриптор буфера активного эк-

рана консоли), дескриптор потока вывода сообщений об ошибках STDERR (как правило, совпадает с дескриптором вывода). Используя вызов GetStdHandle можно получить любой из указанных дескрипторов. Кроме того, используя вызов SetStdHandle можно переустановить дескриптор, т.е. перенаправить ввод и/или вывод в другой поток, например в файл. Таким образом, используя перенаправление стандартных потоков и механизм наследования, можно организовать взаимодействие родитель-потомок с помощью анонимных каналов. Последовательность шагов процесса-родителя для организации передачи данных от процесса-родителя к процессу-потомку можно представить следующим образом (рис. 5.1):

2.GetStdHandle

Родительский процесс

 

5.SetStdHandle

STDOUT

hSaved

STDIN

 

hReadPipe

STDERR

3.SetStdHandle

hWritePipe

 

 

1.CreatePipe

 

 

 

анонимный

 

 

канал

 

 

Дочерний процесс

STDOUT

 

 

STDIN

 

STDERR

4.CreateProcess

 

 

 

ссылка, значение

передача данных наследование

Рис. 4.1. Схема организации симплексной связи с использованием анонимного канала, стандартных потоков ввода-вывода и наследования.

1. Создается анонимный канал, который представляется двумя дескрипторами: дескриптором потока ввода и дескриптором потока вывода. При создании канала поля структуры типа

SECURITY_ATTRIBUTES должны быть установлены так, чтобы создаваемые дескрипторы были наследуемыми.

2.Сохраняется дескриптор стандартного потока ввода.

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

4.Создается процесс-потомок, который должен унаследовать, в том числе, и стандартные потоки ввода-вывода.

5.Восстанавливается стандартный поток ввода.

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

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

4.2. Задание

1.Разработать и отладить программу, представляющую процессродитель и выполняющую следующие функции:

1.1.создание анонимного канала;

1.2.создание (запуск) процесса-потомка (см. п.2 и п.3), у которого стандартный поток ввода перенаправлен на поток ввода анонимного канала;

1.3.передачу потока данных через канал процессу-потомку.

2.Разработать и отладить программу, представляющую процесспотомок и выполняющую следующую функции:

2.1.получение данных от процесса-родителя через стандартный поток ввода;

2.2.форматированный вывод полученных данных в стандартный поток вывода;

3.Проверить работу канала передачи данных при организации взаимодействия между процессом-родителем и процессом-потомком для двух вариантов:

3.1.в качестве процесса-потомка использовать разработанную программу;

3.2.в качестве процесса-потомка использовать стандартную ути-

литу Windows.

4.3.Контрольные вопросы

1.Что такое объект ядра? Приведите примеры таких объектов.

2.Почему анонимный канал можно использовать только для организации взаимодействия между родственными процессами?

3.Какие способы организации совместного использования объектов ядра предоставляет Windows?

4.Какие стандартные потоки ввода-вывода могут использоваться консольным процессом?

5.Как работает механизм наследования в Windows? Каковы его особенности с точки зрения управления наследованием?

6.Какие средства взаимодействия процессов предоставляет Windows?

7.Каким образом процесс может получить доступ к объектам ядра, созданным вне этого процесса?

5. Синхронизация процессов/потоков в Windows

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

5.1. Общие сведения

Объекты синхронизации ядра. Операционная система

Windows предлагает набор специализированных объектов, создающихся на уровне ядра ОС, которые вместе с функциями типа Wait… позволяют решать самые различные задачи по синхронизации потоков и процессов (точнее потоков одного и разных процессов). В таблице 1 представлены основные системные вызовы, предназначенные для работы с объектами синхронизации. Кроме того, синхронизацию потоков, принадлежащих одному процессу, можно выполнять с использованием механизма критических секций (см. таблицу). Использование критических секций, в отличие от объектов ядра, не приводит к переключению в привилегированный режим, что повышает скорость выполнения операций, связанных с синхронизацией. Помимо указанных механизмов, Windows предлагает группу системных вызовов вида Interlocked… для выполнения некоторых действий как атомарных, т.е. операционная система гарантирует, что выполнение такого действия (например, обмен значений двух переменных) не будет прервано и управление не будет передано другому потоку.

Таблица 1.1

Средства синхронизации Windows

Механизм син-

Системные вызовы

Примечание

Создание, откры-

Управление,

хронизации

тие, удаление

использование

 

 

 

 

 

1

2

3

 

4

 

 

 

Объект

«событие»

 

 

PulseEvent

может использовать-

Событие

CreateEvent

ся для

оповещения

ResetEvent

одного или несколь-

(Event)

OpenEvent

SetEvent

ких потоков о на-

 

 

 

ступлении

 

 

 

некоторого события

1

2

3

 

4

 

 

 

 

 

Объект

«мьютекс»

 

CreateMutex

 

может использовать-

Мьютекс

ReleaseMutex

ся для

организации

(Mutex)

OpenMutex

монопольного

вла-

 

 

 

дения

некоторым

 

 

 

ресурсом.

 

 

 

 

 

Объект

«семафор»

 

 

 

может использовать-

Семафор

CreateSemaphor

ReleaseSemapho

ся для

организации

владения некоторым

(Semaphore)

e

re

ресурсом

ограни-

OpenSemaphore

 

 

ченным количеством

 

 

 

клиентов (например,

 

 

 

потоков).

 

 

 

 

 

Объект

 

«таймер»

 

CreateWaitable

SetWaitableTim

может использовать-

 

ся для

синхрониза-

Таймер

Timer

er

ции

выполнения

(Waitable Timer)

OpenWaitableTi

CancelWaitable

потоков с необходи-

 

mer

Timer

мыми

временными

 

 

 

моментами.

 

 

 

 

EnterCriticalS

Механизм «критиче-

 

InitializeCrit

ская секция» ис-

Критическая

ection

пользуется

 

для

секция

icalSection

LeaveCriticalS

решения

задачи

(Critical Section)

DeleteCritical

ection

взаимоисключения в

 

Section

TryEnterCritic

рамках

одного

про-

 

 

alSection

цесса.

 

 

 

5.2.Задание

Вприложении В приведен листинг многопоточной программы, которая моделирует задачу «производители-потребители» («producersconsumers») с ограниченным буфером без использования каких-либо механизмов синхронизации. Модифицировать и отладить программу так, чтобы задача решалась корректно при любом количестве производителей и потребителей и их произвольной относительной скорости работы. При решении задачи должен использоваться весь исходный код программы. Решение считается приемлемым, если в ходе работы программы потоки получают монопольный доступ к буферу и не появляются сообщения о считывании неверных значений и потери информации.

5.3. Контрольные вопросы

1.Какие механизмы синхронизации, предоставляемые Windows , могут использоваться для решения задачи взаимоисключения?

2.Каким образом можно было организовать синхронизацию потоков в третьей работе не прибегая к использованию системных функций

типа WaitFor…?

3.В чем принципиальное отличие использования именованных объектов синхронизации от механизма применения критической секции?

4.В чем сходство и в чем отличия мьютексов и семафоров?

5.Чем отличается активное ожидание, организованное, например, с помощь цикла while (…) {…}, от ожидания события, основанного на использовании функций группы Wait…?

6. Взаимодействие процессов c использованием сокетов

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

6.1. Общие сведения

Сокеты. В основе большинства приложений для Internet лежит организация удаленного взаимодействия. Большинство решений такой организации взаимодействия опирается на механизм сокетов (sockets). Сокеты находятся на промежуточном, так называемом транспортном уровне семиуровневой модели OSI (Open Systems Interconnection). Под ним, на сетевом уровне, находится протокол IP. Над ним находятся протоколы сеансового уровня (сервисы), ориентированные на конкретные задачи, например, HTTP, FTP, SMTP и другие. Использование сокетов, с одной стороны, позволяет уйти от трудоемкой работы на нижних уровнях, с другой – решать широкий круг задач, недоступный специализированным протоколам.

В соответствии с формальным определением считается, что сокет – это совокупность IP-адреса, номера порта и типа протокола (TCP или UDP). Таким образом, сокет – это модель одного конца сетевого соединения, со всеми свойствами и возможностью читать и записывать данные. Обобщенные структурные схемы организации ненадежного (на основе UDP) и надежного (на основе TCP) соединения при помощи сокетов показаны на рисунках 1а и 1б соответственно.

 

 

 

 

sendto, recfrom

 

 

 

 

сервер

 

 

 

сокет

 

 

 

сокет

 

 

 

клиент

 

 

 

 

сервера

 

 

 

клиента

 

 

 

 

 

 

socket

 

 

 

 

socket

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bind

 

a

 

 

 

 

 

 

 

listen

 

 

 

connect

 

 

 

 

 

сервер

прослушива

сокет

клиент

 

 

socket

ющий сокет

 

 

 

клиента

 

socket

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

bind accept send, recv

сокет

сервера

б

Рис. 1. Схема взаимодействия процессов с использованием сокетов: а – при ненадежном соединении, б – при надежном.

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

Сервер

Клиент

Инициализация

Инициализация

библиотеки сокетов

библиотеки сокетов

(WSAStartup)

(WSAStartup)

Создание сокета

Создание сокета

(socket)

(socket)

Привязка сокета

 

(bind)

 

Отправка и получение

Отправка и получение

данных

данных

(sendto, recvfrom)

(sendto, recvfrom)

Закрытие сокета

Закрытие сокета

(closesocket)

(closesocket)

Освобождение

Освобождение

библиотеки сокетов

библиотеки сокетов

(WSACleanup)

(WSACleanup)

 

a

Сервер

Клиент

Инициализация

Инициализация

библиотеки сокетов

библиотеки сокетов

(WSAStartup)

(WSAStartup)

Создание сокета

Создание сокета

(socket)

(socket)

Привязка сокета

 

(bind)

 

Прослушивание

Запрос соединения

соединения

(connect)

(listen)

 

Прием соединения и

 

создание сокета

 

(accept)

 

Отправка и получение

Отправка и получение

данных

данных

(send, recv)

(send, recv)

Закрытие сокета

Закрытие сокета

(сокетов)

(closesocket)

(closesocket)

 

Освобождение

Освобождение

библиотеки сокетов

библиотеки сокетов

(WSACleanup)

(WSACleanup)

 

б

Рис. 2. Последовательностьдействийнеобходимых дляорганизации взаимодействия процессов с использованием сокетов: а – приненадежном соединении, б– при надежном.

Прежде всего необходимо отметить, что для использования интерфейса WinSock необходимо подключение заголовочного файла winsock2.h. Вызов WSAStartup необходим для инициализирует библиотеки, необходимые для работы сокетов. В качестве первого аргумента функция ожидает номер версии, в качестве второго – указатель на структуру WSAData, которая после успешного выполнения будет содержать сведения о текущей версии WinSock и ее настройках.

Создание сокета осуществляется с помощью вызова socket, который ожидает четыре параметра. Первый параметр задает семейство адресов, наиболее часто используются два значения AF_INET (для организации удаленного взаимодействия) и AF_LOCAL (для организации локального взаимодействия). Второй параметр определяет тип сокета и соответственно тип протокола – SOCK_DGRAM для использования UDP и SOCK_STREAM для использования TCP. Третий параметр используется в тех случаях, когда тип сокета неоднозначно определяет тип протокола, в других случаях может быть равен 0. В случае удачно-

го выполнения вызов возвращает дескриптор сокета (типа SOCKET), либо значение INVALID_SOCKET в случае ошибки.

Вызов bind осуществляет привязку сокета (его дескриптор передается в качестве первого параметра) к адресу (второй параметр), определяемого структурой типа sockaddr_in, которая включает:

поле sin_family – семейство адресов;

поле sin_port – номер порта;

поле sin_addr – структура, поле s_addr, которой присваивается значение INADDR_ANY, если ожидается связь с любым адресом, либо адрес конкретного хоста (в случае известна строка, представляющая IPадрес, ее можно преобразовать в необходимый формат с помощью функции inet_addr).

Третий параметр – это размер структуры типа sockaddr_in, которая передается в вызов.

Использование вызовов recv, send и recvfrom, sendto

аналогично использованию вызовов WriteFile, ReadFile. Отличие в том, что в качестве первого параметра необходимо передавать дескриптор сокета. Кроме того, при использовании вызовов recvfrom и sendto необходимо указывать адрес сокета (передается через указатель на структуру sockaddr_in) – партнера по взаимодействию и размер этого адреса.

Закрытие сокета осуществляется с помощью вызова closesocket. Для того, чтобы предоставить возможность отправить/получить данные перед разрывом соединения желательно использовать вызов shutdown, предваряя им закрытие сокета. Выгрузка библиотек, поддерживающих взаимодействие через WinSock осуществляется с помощью WSACleanup, который, кроме прочего, освобождает выделенные ранее ресурсы.

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

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

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

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

6.2. Задание

Разработать приложения1, представляющие серверную и клиентскую стороны, которые реализуют (варианты по выбору):

1.игру «Морской бой»;

2.игру «Крестики-нолики»;

3.программу передачи текстовых сообщений по сети.

6.3.Контрольные вопросы

1.Какие средства взаимодействия предоставляет Windows?

2.В чем заключается «надежность» и «ненадежность» соединения, получаемого с помощью сокетов?

3.Можно ли организовать локальное взаимодействие процессов с помощью сокетов?

4.Каким образом можно организовать одновременное взаимодействие сервера с несколькими клиентами?

1 Для реализации приложений необходимо спроектировать протокол взаимодействия сервера и клиента.

Приложение A. Краткий справочник по некоторым функциям Windows API

Функции работы с процессами

UINT WinExec(LPCSTR lpCmdLine, UINT uCmdShow);

Функция предназначена для запуска приложений Windows. Используется для обеспечения совместимости с предыдущими версиями Windows. Функция возвращает значение > 31 в случае успешного выполнения, код ошибки в противном случае.

lpCmdLine

Указатель на нуль-терминальную строку символов, содер-

 

жащую командную строку запускаемого приложения

 

(процесса).

uCmdShow

Значение, определяющее характер открытия окна прило-

 

жения (одна из констант, например SW_SHOW).

HINSTANCE ShellExecute(HWND hwnd, LPCTSTR lpOperation, LPCTSTR lpFile, LPCTSTR lpParameters, LPCTSTR lpDirectory, INT uShowCmd);

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

hwnd

Дескриптор родительского окна.

lpOperation

Указатель на нуль-терминальную строку символов, содер-

 

жащую команду. Допустимы следующие значения (значе-

 

ние lpOperation можеть равно NULL, в этом случае

 

функция открывает файл, указанный в lpFile):

 

"open" – функция открывает файл, указанный в lpFile,

 

причем этот файл может быть исполняемым, документом

 

или директорией.

 

"print" – функция отправляет на печать файл, указан-

 

ный в lpFile, который должен быть документом, в про-

 

тивном случае открывает его.

 

"explore" – функция открывает директорию, указанную

lpFile

в lpFile с помощью Explorer.

Указатель на нуль-терминальную строку символов, содер-

 

жащую путь к файлу, над которым необходимо выполнять

 

заданную команду.

lpParameters

Указатель на нуль-терминальную строку символов, содер-

 

жащую параметры для исполняемого файла. Если откры-

 

вается документ, то параметр может иметь значение NULL.

lpDirectory Указатель на нуль-терминальную строку символов, содержащую путь к директории по умолчанию.

uCmdShow Значение, определяющее характер открытия окна приложения (одна из констант, например SW_SHOW).

BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);

Функция создает процесс и его основной поток, созданный процесс исполняет указанный файл. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

lpApplicationNa

Указатель на нуль-терминальную строку символов, содер-

me

жащую путь к исполняемому модулю. Может быть равен

 

NULL, в этом случае имя файла, отделенное пробелом,

lpCommandLine

должно быть указано в параметре lpCommandLine.

Указатель на нуль-терминальную строку символов, содер-

 

жащую командную строку.

lpProcessAttrib

Указатель на структуру, описывающую атрибуты безопас-

utes

ности создаваемого процесса и задающую возможность

 

наследования создаваемого дескриптора процесса.

lpThreadAttribu

Аналогичен предыдущему, но для главного потока созда-

tes

ваемого процесса.

bInheritHandles

Указывает, наследует ли создаваемый процесс дескрипто-

 

ры текущего процесса.

dwCreationFlags

Набор флагов, определяющий создание процесса и его

 

приоритетность.

lpEnvironment

Указатель на блок переменных окружения процесса.

lpCurrentDirect

Указатель на нуль-терминальную строку символов, содер-

ory

жащую путь текущей директории.

lpStartupInfo

Указатель на структуру STARTUPINFO, задающую пара-

 

метры создания процесса и его запуска.

lpProcessInform

Указатель на структуру PROCESS_INFORMATION, в кото-

ation

рую заносятся данные о создаваемом процессе.

HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);

Функция возвращает дескриптор существующего процесса в случае успешного завершения, NULL в противном случае.

dwDesiredAccess Набор флагов, определяющий доступ к процессу.

bInheritHandle Флаг, указывающий, является ли создаваемый дескриптор наследуемым.

dwProcessId Идентификатор процесса.

BOOL GetProcessTimes(HANDLE hProcess, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime);

Получает информацию о временных параметрах процесса. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hProcess

Дескриптор процесса.

lpCreationTime

Указатель на структуру, содержащую время создания про-

 

цесса.

lpExitTime

Указатель на структуру, содержащую время завершения

 

процесса.

lpKernelTime

Указатель на структуру, содержащую время нахождения

 

процесса в привилегированном режиме.

lpUserTime

Указатель на структуру, содержащую время нахождения

 

процесса в пользовательском режиме.

BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);

Функция завершает выполнение процесса и всех его потоков. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hProcess

Дескриптор процесса.

uExitCode

Код завершения процесса.

Функции работы с потоками

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);

Функция создает в адресном пространстве процесса, вызвавшего ее, поток исполнения. Функция возвращает дескриптор процесса в случае успешного выполнения, NULL в противном случае.

Указатель на структуру типа SECURITY_ATTRIBUTES, lpThreadAttribu определяющую, может ли наследоваться возвращаемый

tes функцией дескриптор потока. Если параметр равен NULL, то дескриптор наследоваться не может.

dwStackSize Значение, задающее размер стека. Если значение параметра равно 0, то создается стек, имеющий размер, заданный по умолчанию.

lpStartAddress Адрес старта потока, обычно это адрес функции-потока.

lpParameter Нетипизированный указатель на значение, передаваемое в поток.

dwCreationFlags Дополнительные флаги, управляющие созданием потока.

lpThreadId Параметр, в который заносится идентификатор создаваемого потока.

int GetThreadPriority(HANDLE hThread);

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

THREAD_PRIORITY_ABOVE_NORMAL (THREAD_PRIORITY_HIGHEST) –

значение приоритета потока на 1 (2) выше класса приоритета процесса;

THREAD_PRIORITY_BELOW_NORMAL (THREAD_PRIORITY_LOWEST) –

значение приоритета потока на 1 (2) ниже класса приоритета процесса;

THREAD_PRIORITY_IDLE (THREAD_PRIORITY_TIME_CRITICAL) –

базовый уровень приоритета потока равен 1 (15) для классов приоритетов процессов IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS,

HIGH_PRIORITY_CLASS, или 16 (31) для приоритетов класса

REALTIME_PRIORITY_CLASS;

THREAD_PRIORITY_NORMAL – приоритет потока равен базовому приоритету процесса.

Впротивном случае, возвращается значение

THREAD_PRIORITY_ERROR_RETURN.

hThread

Дескриптор потока.

int SetThreadPriority(HANDLE hThread, int nPriority);

Функция устанавливает относительное значение приоритета потока, которое вместе со значением приоритета процесса определяет базовый приоритет потока. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hThread

Дескриптор потока.

nPriority Назначаемое значение приоритета (см. описание функции

GetThreadPriority).

BOOL GetThreadPriorityBoost(HANDLE hThread, PBOOL pDisablePriorityBoost);

Функция позволяет получить режим изменения приоритета указанного потока. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hThread

Дескриптор потока.

pDisablePriorit Указатель на переменную, значение которой, определяет yBoost режим изменения приоритета потока: TRUE – динамическое изменение приоритета запрещено, FALSE – разреше-

но.

BOOL SetThreadPriorityBoost(HANDLE hThread, BOOL

DisablePriorityBoost);

Функция устанавливает режим изменения приоритета указанного потока. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hThread

Дескриптор потока.

DisablePriority Значение, определяющее режим изменения приоритета Boost потока: TRUE – динамическое изменение приоритета за-

прещено, FALSE – разрешено.

BOOL TerminateThread (HANDLE hThread, DWORD uExitCode);

Функция завершает выполнение указанного потока. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hThread

Дескриптор потока.

uExitCode

Код завершения потока.

BOOL GetThreadTimes(HANDLE hThread, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime);

Получает информацию о временных параметрах потока. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hProcess

Дескриптор процесса.

lpCreationTime

Указатель на структуру, содержащую время создания по-

 

тока.

lpExitTime

Указатель на структуру, содержащую время завершения

 

потока.

lpKernelTime

Указатель на структуру, содержащую время нахождения

 

потока в привилегированном режиме

lpUserTime

Указатель на структуру, содержащую время нахождения

 

потока в пользовательском режиме.

Функции синхронизации

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

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

WAIT_ABONDED – используется при работе с мьютексами;

WAIT_OBJECT_0 – объект перешел в сигнальное состояние;

WAIT_TIMEOUT – закончился временной интервал ожидания.

Вслучае неуспешного выполнения – WAIT_FAILED.

hHandle

Дескриптор объекта, состояние которого контролируется.

dwMilliseconds Значение временного интервала ожидания в миллисекундах. Если в качестве значения задана константа INFINITE, временной интервал не ограничен.

DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);

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

вслучае успешного выполнения одно из следующих значений:

от WAIT_ABONDED до (WAIT_ABONDED + nCount – 1) – используется при работе с мьютексами;

от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount – 1) – если значение bWaitAll равно TRUE, то возвращаемое значение означает, что все объекты перешли в сигнальное состояние; если значение bWaitAll равно FALSE, то возвращаемое значение минус WAIT_OBJECT_0 равно индексу объекта, перешедшего в сигнальное состояние;

WAIT_TIMEOUT – закончился временной интервал ожидания.

В случае неуспешного выполнения – WAIT_FAILED.

nCount

Количество объектов, дескрипторы которых переданы в

lpHandles

массиве, на который указывает lpHandles.

Указатель на массив дескрипторов контролируемых объек-

 

тов.

bWaitAll

Флаг, определяющий тип ожидания. Если значение флага

 

равно true, то функция ожидает, когда все объекты, деск-

 

рипторы которых указаны в lpHandles, перейдут в сиг-

 

нальное состояние. Если значение флага равно false, то

 

функция ожидает, когда хотя бы один объект из массива

 

объектов, дескрипторы которых указаны в lpHandles,

 

перейдет в сигнальное состояние.

dwMilliseconds

Значение временного интервала ожидания в миллисекун-

 

дах. Если в качестве значения задана константа

 

INFINITE, временной интервал не ограничен.

Возвращаемый дескриптор потока ввода создаваемого канала (указатель).
Возвращаемый дескриптор потока вывода создаваемого канала (указатель).
Указатель на структуру типа SECURITY_ATTRIBUTES,

Прочие функции

BOOL CreatePipe(PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize);

Функция создает анонимный канал и возвращает его дескрипторы потоков ввода и вывода. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hReadPipe

hWritePipe

lpPipeAttribute

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

nSize

Определяет размер буфера канала. Если задано нулевое

 

значение, то назначается размер буфера по умолчанию.

HANDLE GetStdHandle(DWORD nStdHandle);

Функция возвращает дескриптор одного из стандартных потоков ввода-вывода. в случае успешного выполнения, в противном случае NULL.

nStdHandle Указывает, дескриптор какого именно потока необходимо получить:

STD_INPUT_HANDLEстандартного потока ввода;

STD_OUTPUT_HANDLE

стандартного потока

вывода;

 

STD_ERROR_HANDLEстандартного потока вывода ошибок.

BOOL SetStdHandle(DWORD nStdHandle, HANDLE hHandle);

Назначает дескриптор одного из стандартных потоков ввода-вывода. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

nStdHandle Указывает, дескриптор какого именно потока необходимо получить (STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE).

hHandle

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

 

BOOL

DuplicateHandle(HANDLE

hSourceProcess,

HANDLE

hSource, HANDLE hTargetProcess, PHANDLE lpTarge, DWORD dwDesiredAccess, BOOL bInheriHANDLE, DWORD dwOptions);

Создает дубликат дескриптора. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hSourceProcess

Дескриптор процесса, владеющего дескриптором, копию

 

которого необходимо создать.

hSource

Дескриптор, подлежащий копированию.

hTargetProcess

Дескриптор процесса, который будет владеть копией деск-

 

риптора.

lpTargeHANDLE

Указатель на создаваемый дубликат дескриптора.

dwDesiredAccess

Определяет режим доступа к дескриптору-копии.

bInheriHANDLE

Задает режим наследования.

dwOptions

Опциональное значение.

BOOL CloseHandle(HANDLE hObject);

Функция закрывает открытый дескриптор объекта. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hObject

Дескриптор объекта.

BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);

Функция записи данных в файл и другие потоки ввода. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

hFile

Дескриптор потока ввода (возможно файла).

lpBuffer

Указатель на буфер, содержащий данные, которые необхо-

 

димо записать.

nNumberOfBytesT

Размер объема данных, которые необходимо записать, в

oWrite

байтах.

lpNumberOfBytes

Указатель на переменную, хранящую количество записан-

Written

ных байтов.

lpOverlapped

Указатель на структуру данных, задающие специфический

 

способ записи данных.

BOOL FileTimeToSystemTime(CONST FILETIME *lpFileTime, LPSYSTEMTIME lpSystemTime);

Функция выполняет преобразование времени, представленного структурой FILETIME (единица измерения 100 нс) к системному времени, представленному структурой SYSTEMTIME. Функция возвращает значение TRUE в случае успешного выполнения, FALSE – в противном случае.

lpFileTime

Указатель на структуру FILETIME.

lpSystemTime

Указатель на структуру SYSTEMTIME.

Приложение Б. Создание проекта в Visual Studio

Процесс создания консольного приложения в среде разработки Visual Studio представлен на следующих рисунках.

Рис. Б. 1. Выбор типа создаваемого объекта – проект

Рис. Б. 2. Выбор типа приложения – Win32, Win32 Console Application,

а также указание имени проекта и его месторасположения

Рис. Б. 3. Выбор закладки настроек приложения (Application Settings)

Рис. Б. 4. Задание настроек – Console Application и Empty Project

После этого необходимо создать как минимум один модуль (unit). Для этого в обозревателе решения (Solution Explorer), необходимо правой кнопкой мыши щелкнуть на ветви Source Files (исходные файлы) и выбрать необходимый пункт в контекстном меню (рис. Б. 5 и рис. Б. 6)

Рис. Б. 5. Выбор типа добавляемого файла в обозревателе решения

Рис. Б. 6. Выбор типа операции и категории добавляемого объекта

Впоявившемся диалоговом окне необходимо указать тип файла (C++)

изадать имя создаваемого модуля (рис. Б. 7).

Рис. Б. 7. Диалоговое окно настроек создаваемого объекта

В результате будет создан проект консольного приложения, включающий один модуль исходного кода (пустой) – рис. Б. 8.

Рис. Б. 8. «Заготовка» проекта консольного приложения

Приложение В. Программная модель задачи синхронизации «Производители потребители»

/*-----------------------------------

Интерфейс модели буфера (заголовочный файл). Правки не допускаются.

------------------------------------*/ #ifndef bufferH

#define bufferH #include <iostream> #include <windows.h> using namespace std; class Buffer { protected:

Buffer(){}

static Buffer * Buf; public:

static Buffer * CreateBuffer(int); virtual int GetItem(void) = 0; virtual void PutItem(int) = 0; virtual ~Buffer(){}

};

#endif

/*-------------------------------------

Реализация (модуль) модели буфера. Правки не допускаются.

---------------------------------------*/ #include "buffer.h"

Buffer * Buffer::Buf = 0; class _Buffer: public Buffer

{

private:

int _Capacity; int _Count; int _Anybody; int * _b;

int _GetItem(void); void _PutItem(int V);

public:

virtual int GetItem(void); virtual void PutItem(int V);

_Buffer(int _Cap):_Capacity(_Cap), _Count(0), _Anybody(0) { if (_Cap > 0) _b = new int[_Cap];

else throw "Capacity must be greater then zero!";

}

virtual ~_Buffer() {if(_b) delete [] _b;}

};

Buffer * Buffer::CreateBuffer(int _Cap) {

if(Buf) return Buf;

else return Buf = new _Buffer(_Cap);

}

int _Buffer::GetItem(void) { if (_Anybody > 0) {

cout << "Buffer is busy!\n"; return 0;

}

_Anybody++;

if (_Count <= 0) {

cout << "Buffer is empty!\n"; _Anybody--;

return 0;

}

return _GetItem();

}

void _Buffer::PutItem(int V)

{

if (_Anybody > 0) {

cout << "Buffer is busy!\n"; return;

}

_Anybody++;

if (_Count >= _Capacity) { cout << "Buffer is full!\n"; _Anybody--;

return;

}

_PutItem(V); return;

}

int _Buffer::_GetItem(void){ Sleep(rand()%20); _Anybody--;

return _b[--_Count];

}

void _Buffer::_PutItem(int V) { Sleep(rand()%20); _Anybody--;

_b[_Count++] = V; return;

}

/*------------------------------------------------------

Модель задачи синхронизации "Производители-потребители". Основной модуль.

#include--------------------------------------------------------<windows.h>

*/

 

#include <iostream>

 

#include "buffer.h"

/*Количество производителей*/

#define cProducers 3

#define cConsumers 5

/*Количество потребителей*/

#define BufferSize 5

/*Размер буфера*/

int cOperations = 1000; /*Количество операций над буфером*/

DWORD __stdcall getkey(void * b) { cin.get();

return cOperations = 0;

}

/* Изменения в программе должны осуществляться только в главном модуле и только путем добавления кода, позволяющего синхронизировать действия:

-внутри "производителей" и "потребителей";

-в теле главной функции (создание объектов синхронизации и пр.);

-в глобальном пространстве (объявление переменных и пр.).

*/

/*Исходный вариант потока-производителя*/ DWORD __stdcall producer(void * b) {

while (cOperations-- > 0) { int item = rand(); ((Buffer*)b)->PutItem(item); Sleep(500 + rand()%100);

}

return 0;

}

/*Исходный вариант потока-потребителя*/ DWORD __stdcall consumer(void * b) {

while (cOperations-- > 0) {

cout << ((Buffer*)b)->GetItem() << endl; Sleep(500 + rand()%100);

}

return 0;

}

int main() {

Buffer * Buf = Buffer::CreateBuffer(5); /*Создание буфера*/ HANDLE hThreads[cProducers+cConsumers];

/*Вспомогательный поток, ожидающий нажатие клавиши*/ CreateThread(0,0,getkey,0,0,0);

for(int i = 0; i < cProducers; i++) /*Создание потоков-производителей*/

hThreads[i] = CreateThread(0,0,producer,Buf,0,0); for(int i = cProducers; i < cProducers + cConsumers; i++)

/*Создание потоков-потребителей*/

hThreads[i] = CreateThread(0,0,consumer,Buf,0,0);

WaitForMultipleObjects(cProducers + cConsumers, hThreads, true, INFINITE);

cin.get(); return 0;

}