Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Lection 10.doc
Скачиваний:
16
Добавлен:
13.04.2015
Размер:
207.36 Кб
Скачать

8.2.2. Функции ожидания объектов ядра (wait - функции)

Функции этого типа позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Самой простой и наиболее используемой функцией является WaitForSingleObject. Она имеет два параметра - указатель на объект ядра и интервал времени. Поток, вызвав эту функцию, ждет освобождения объекта ядра, дескриптор которого передан в первом параметре, объект ядра должен поддерживать состояния «занят/свободен». Второй параметр функции указывает, сколько времени поток (вызывающий эту функцию) должен ждать объект ядра, если передать значение INFINITE (0xFFFFFFFF), то поток будет ждать неограниченное время. Функция возвращает такие значения:

  1. WAIT_OBJECT_0 - объект ядра стал свободным

  2. WAIT_TIMEOUT - время ожидания вышло

  3. WAIT_FAILED - ошибка функции (неверный дескриптор), GetLastError

  4. WAIT_ABANDONED – указанный объект является объектом ядра «мьютекс», который не освобожден потоком, которому он принадлежит и этот поток не завершен. Владение объектом «мьютекс» передано вызывающему потоку.

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

  • Изменение уведомления (Change notification),

  • Консольный ввод (Console input),

  • Событие (Event), Мьютекс (Mutex),

  • Процесс (Process),

  • Семафор (Semaphore),

  • Поток (Thread),

  • Таймер (Timer).

Для ожидания сразу нескольких объектов ядра или какого-то одного из нескольких, используется функция WaitForMultipleObjects. Она имеет такие параметры:

  • Параметр nCount определяет количество объектов ядра в массиве lpHandles - это значение может находиться в пределах от 1 до MAXIMUM_WAIT_OBJECTS (определено как 64);

  • Параметр lpHandles - указатель на массив указателей ожидаемых объектов

  • Параметр fWaitAll определяет – ждать освобождения всех объектов ядра сразу (TRUE) или какого-то одного (FALSE);

  • Параметр dwMilliseconds определяет время ожидания объектов в милисекундах.

Возвращаемые значения аналогичны функции WaitForSingleObject за исключением WAIT_OBJECT_0, здесь это значение говорит только о нулевом объекте ядра из массива указателей. Чтобы узнать какой именно объект ядра из указанного массива освободился, необходимо прибавить номер объекта в массиве к значению WAIT_OBJECT_0. Нумерация в массиве указателей начинается с нуля. Это показано в примере 8.3.

Пример 8.3. Применение функции WaitForMultipleObjects

WAIT_OBJECT_0 + 0;

WAIT_OBJECT_0 + 1;

WAIT_OBJECT_0 + 2;

HANDLE hProc[3];

hProc[0] = hMyProc1;

hProc[1] = hMyProc2;

hProc[2] = hMyProc3;

DWORD dwResult = WaitForMultipleObjects( 3, hProc, FALSE, 5000);

switch(dwResult)

{

case WAIT_FAILED:

// неправильный вызов функции (неверный дескриптор?)

break;

case WAIT_TIMEOUT:

// время вышло, ни один из объектов не освободился за 5000 мс

break;

case (WAIT_OBJECT_0 + 0):

// завершился процесс, идентифицируемый hProc[0] или hMyProc1

break;

case (WAIT_OBJECT_0 + 1):

// завершился процесс, идентифицируемый hProc[1] или hMyProc2

break;

case (WAIT_OBJECT_0 + 2):

// завершился процесс, идентифицируемый hProc[2] или hMyProc3

break;

}

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

8.3. Разработка многопоточных приложений в среде Visual C++

В начале разработки многопоточного приложения в среде Visual C++ необходимо определить, с какой библиотекой оно будет работать. Если в программе предполагается использовать функцию _beginthread или другую подобную функцию, то необходимо изменить конфигурацию проекта таким образом, чтобы при компиляции использовалась многопоточная версия библиотеки. Этот параметр настраивается при помощи вкладки C/C++ в разделе Code Generation settings (Настройки генерации кода). Параметр имеет на­звание Use Run-Time Library (Используемая библиотека времени исполнения). В среде разработки доступны следующие библиотеки, указанные в таблице 8.1.

Таблица 8.1. Библиотеки С/С++, которые могут использоваться при разработке приложений

Библиотека

Описание

libc.lib

Статически подключаемая библиотека для однопоточных приложений (используется по умолчанию).

libcd.lib

Отладочный аналог предыдущей библиотеки.

libcmt.lib

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

libcmtd.lib

Отладочный аналог предыдущей библиотеки.

msvcrt.lib

Библиотека импорта для динамического подключения рабочей версии msvcrt.dll, поддерживает однопоточные и многопоточные приложения.

msvcrtd.lib

Отладочный аналог предыдущей библиотеки.

Наличие нескольких библиотек связано со следующими причинами:

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

  • в C/C++ библиотеках многие функции используют глобальные блоки памяти не учитывая того, что в многопоточном приложении любой поток может быть прерван в любой момент и глобальный блок памяти может быть изменён другим потоком.

  • многопоточная версия библиотеки C/C++ обертывает некоторые функции синхронизирующими примитивами (например, функция malloc, выделяющая память из кучи, так как несколько потоков не могут выделять одновременно память из кучи, иначе она может быть повреждена).

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

  • имеется DLL вариант многопоточной библиотеки C/C++, он поддерживает и однопоточные приложения, это наилучший вариант библиотеки на сегодняшний день.

  • при использовании многопоточной библиотеки (компилятору необходимо указать ключ /MT или /MD в командной строке) глобальная переменная заменяется функцией, возвращающей адрес из блока памяти tiddata (для своего потока), в котором и хранится значение глобальной переменной errno для каждого потока.

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

В составе стандартной биб­лиотеки C++ присутствуют функции _beginthreadex, _threadstartex, _endthreadex. Рассмотрим эти функции подробнее.

Функция _beginthreadex. Для того, чтобы многопоточные программы, использующие C/C++ библиотеки, работали корректно, необходимо создать специальную структуру данных и связать её с каждым потоком, который вызывает библиотечные функции. Потоки должны знать, что необходимо обращаться именно к своим структурам данных и не трогать данные других потоков. Создание специальных структур данных полностью возлагается на пользователя (это можно сделать при создании потока функцией).

Все параметры _beginthreadex совпадают с параметрами функции CreateThread из Windows API. При реализации функции _beginthreadex использована функция CreateThread. Реализация функции _beginthreadex находится в файле threadex.c. Также совпадает и возвращаемое значение (дескриптор потока), однако для параметров необходимо выполнить приведение типов. Перечень параметров функции приведен ниже.

unsigned long _beginthreadex(

void* security,

unsigned stack_size,

unsigned (*start_address)(void*),

void* arglist,

unsigned initflag,

unsigned *thrdaddr);

Функция _beginthreadex существует только в многопоточных библиотеках, так как однопоточная библиотека не может правильно обслуживать многопоточное приложение. Каждый поток получает свой блок памяти tiddata, выделяемый из кучи, которая принадлежит библиотеке С/С++. Адрес функции потока и её параметр, переданные _beginthreadex, запоминаются в блоке памяти tiddata. При вызове CreateThread сообщается, что она должна начать выполнение нового потока с функции _threadstartex, а не с того адреса, на который указывает lpStartAddress. Кроме этого, в lpParameter функции CreateThread передаётся не параметр функции потока, а адрес структуры tiddata. Если создание потока пройдет успешно, то будет возвращён дескриптор потока, иначе NULL.

Функция _threadstartex. Она передаётся как функция потока в CreateThread в теле _beginthreadex. Основной задачей threadstartex является сопоставление блока данных tiddata с потоком. Синтаксис функции следующий:

static unsigned long WINAPI _threadstartex ( void * ptd )

Особенности потоков, связанные с _threadstartex состоят в следующем:

  • новый поток начинает выполнение с BaseThreadStart (в kernel32.dll), а затем переходит в _threadstartex;

  • в качестве параметра функции _threadstartex передаётся адрес блока tiddata нового потока;

  • значение функции TlsSetValue сопоставляется со значением, которое называется локальной памятью потока (Thread Local Storage, TLS), а _threadstartex сопоставляет блок tiddata с новым потоком;

  • функция потока заключается в SEH-фрейм, который предназначен для обработки ошибок времени выполнения, поддержки библиотечной функции signal и др;

  • следствие: если поток создать функцией CreateThread и вызвать signal, то последняя будет работать некорректно;

  • далее вызывается функция потока, которой передаётся нужный параметр;

  • значение, возвращаемое функцией потока, считается кодом завершения этого потока;

  • для завершения потока вызывается функция _endthreadex.

Функция _endthreadex. Для завершения потока, созданного _beginthreadex, вызывается _endthreadex. Синтаксис функции следующий:

void __cdecl _endthreadex ( unsigned retcode )

Некоторые особенности, связанные с функцией _endthreadex:

  • библиотечная функция _getptd обращается к Windows-функции TlsGetValue, которая сообщает адрес блока памяти tiddata вызывающего потока;

  • после того, как блок памяти, отведённый под tiddata, освободится, поток будет разрушен функцией ExitThread.

Как упоминалось выше, для реализации функции _beginthreadex используется функция CreateThread. При каждом вызове этой функции операционная система создаёт объект ядра «поток» (компактная структура данных, которая используется операционной системой для управления потоком), выделяет память под стек потока из адресного пространства. Однако этот поток является пустым, из-за этого функция CreateThread используется редко при разработке приложений. Синтаксис функции CreateThread следующий:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,

DWORD dwStackSize,

LPTHREAD_START_ROUTINE lpStartAddress,

LPVOID lpParameter,

DWORD dwCreationFlags,

LPDWORD lpThreadId

);

Параметр lpThreadAttributes - это указатель на структуру, содержащую флаг наследования (если bInheritHandle = TRUE, то дескриптор будет наследован дочерними процессами) и дескриптор защиты – SECURITY_ATTRIBUTES.

typedef struct _SECURITY_ATTRIBUTES {

DWORD nLength;

LPVOID lpSecurityDescriptor;

BOOL bInheritHandle;

} SECURITY_ATTRIBUTES;

Параметр dwStackSize определяет, какую часть адресного пространства поток сможет использовать под свой стек. При инициализации первичного потока приложения, функция CreateProcess заносит в этот параметр значение, встроенное компоновщиком в исполняемый файл. Управлять этим значением позволяет ключ компоновщика: /STACK: [reserve] [,commit] . Аргумент reserve – максимально допустимый резервируемый объём памяти в байтах под стек потока (по умолчанию – 1 Мб). Аргумент commit – задаёт объём физической памяти в байтах, который изначально передается области, зарезервированной под стек (по умолчанию 1 страница). При расширении стека потока, возможно, его размер превысит изначально выделенный объём (аргумент commit) под стек, тогда система будет увеличивать резервируемую память под стек (прибавлять commit байт) до тех пор, пока стек не вырастет до reserve байт. Во время переполнения стека происходит исключение, которое перехватывает система и добавляет заданное количество байт. Параметр dwStackSize определяет максимально допустимый резервируемый объём памяти для потока, если указать 0, то будет использовано значение, встроенное компоновщиком в исполняемый файл, то есть в /STACK. Ограничение резервируемого объёма памяти под стек позволяет прекратить работу функции с бесконечной рекурсией.

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

Через параметр lpParameter передаётся некоторое инициализирующее значение в параметр функции потока (имеют одинаковый тип). Поскольку ОС Windows имеет вытесняющую многозадачность, то родительский и дочерний потоки могут выполняться одновременно.

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

  • 0 - исполнение потока начинается немедленно;

  • CREATE_SUSPENDED – система создаёт поток, инициализирует его и приостанавливает его до последующих указаний, это позволяет изменить какие-либо свойства потока перед тем, как он выполнит код.

Параметр lpThreadId. Адрес переменной DWORD, в которую функция возвращает идентификатор, приписанный системой новому потоку. В Windows NT/2000 можно передавать NULL, говоря, что идентификатор не нужен, но в Windows 9x это приведёт к ошибке.

Для того, чтобы приостановить работу потока на определенное время можно использовать функцию Sleep. Параметром функции служит время ожидания потока в миллисекундах. Вызывать эту функцию нужно из функции потока. Есть также функция SleepEx, которая переводит текущий поток в режим ожидания до возникновения одной из следующих ситуаций: завершение ввода/вывода, поступление в поток вызова асинхронной процедуры (asynchronous procedure call -APC), завершение временного интервала ожидания.

При завершении потока освобождаются все дескрипторы User-объектов, принадлежавших потоку (ему может принадлежать только два типа User-объекта, это окна и ловушки, остальные принадлежат процессу), другие User-объекты разрушаются во время завершения процесса. Код завершения потока меняется со STILL_ACTIVE на код, переданный в функции ExitThread или TerminateThread. Объект ядра «поток» переводится в свободное состояние. Если данный поток является последним потоком в процессе, завершается и сам процесс. Счетчик пользователей объекта ядра «поток» уменьшается на 1.

При завершении потока, объект ядра «поток» будет существовать до тех пор, пока не будут закрыты все его дескрипторы. Такой объект ядра может быть нужен только для получения кода завершения потока при помощи функции GetExitCodeThread. Поток можно завершить одним из способов указанных ниже.

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

2. Поток уничтожается вызовом функции ExitThread. В этом случае освобождаются только ресурсы, выделенные под стек потока, но не ресурсы, выделенные C++ объектам. Лучше использовать функцию _endthreadex.

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

4. Завершается процесс, содержащий данный поток при помощи функций ExitProcess и TerminateProcess, принудительно завершают все потоки, принадлежавшие процессу, как будто вызывают для каждого потока TerminateThread. Все ресурсы будут освобождены системой, но деструкторы C++ объектов вызваны не будут, возможно, какие-то данные не будут записаны на диск.

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

Для решения задач синхронизации в MFC существует 6 специальных классов, которые деляться на две категории. Первая категория это объекты синхронизации CSyncObject, CSemaphore, CMutex, CCriticalSection и CEvent. Вторая категория - это объекты синхронного доступа CMultiLock и CSingleLock. Методы синхронизации первой категории используются тогда, когда необходимо гарантировать целостность ресурса при доступе к нему. Вторая категория методов синхронизации используется для получения доступа к этим ресурсам. Для того чтобы определить, какой из методов синхронизации необходимо использовать, следует руководствоваться следующими принципами. Если приложение должно ожидать некоего события, перед тем как будет осуществлено какое-то действие, то необходимо использовать класс CEvent. Если в приложении более чем один поток одновременно должен получать доступ к одному и тому же ресурсу, то необходимо использовать класс CSemaphore. Если более чем одно приложение будет использовать одновременно один и тот же ресурс, например DLL, то необходимо использовать класс CMutex, в противном случае используется класс CCriticalSection. Класс CSyncObject никогда не используется напрямую. Это базовый класс для всех классов синхронизации.

Если приложение будет монопольно работать с каким-либо ресурсом, то необходимо использовать класс CSingleLock. Если необходимо обеспечивать доступ к одному из многих используемых ресурсов, то необходимо использовать CMultiLock. Подробно о работе с этими функциями можно почитать в [10].

В таблице 8.2 приведен перечень Windows API функций для создания, удаления потоков, а также управления ими.

Таблица 8.2. Функции Windows API для управления потоками

Функция

Описание

AttachThreadInput

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

CreateRemoteThread

Создает поток в другом процессе

CreateThread

Создает поток в текущем процессе

ExitThread

Завершает работу текущего потока

GetCurrentThread

Возвращает дескриптор текущего потока

GetCurrentThreadId

Возвращает идентификатор ID

текущего потока

GetExitCodeThread

Возвращает код завершения потока

GetThreadPriority

Возвращает уровень приоритета потока

GetThreadPriorityBoost

Возвращает статус динамического изменения приоритета для данного процесса

GetThreadTimes

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

OpenThread

Открывает существующий объект потока

ResumeThread

Продолжает работу потока, приостановленного в результате обращения к SuspendThread

SetThreadAffinityMask

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

SuspendThread

Временно приостанавливает выполнение потока

SetThreadIdealProcessor

Устанавливает, какой процессор предпочтительней использовать для выполнения потока

SetThreadPriority

Устанавливает уровень приоритета потока

SetThreadPriorityBoost

Разрешает или запрещает динамическое изменение уровня приоритета данного потока

Sleep

Приостанавливает работу потока на определенный интервал времени

SleepEx

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

SwitchToThread

Переключает процессор на выполнение другого потока (неизвестно, какого именно)

ThreadProc

Программно определяемая функция, которая служит для указания начального адреса потока

TlsAlloc

Выделяет локальную память потока (TLS - Thread Local Storage)

TlsFree

Освобождает локальную память потока

TlsGetValue

Читает значение TLS для указанного индекса TLS в вызываемом потоке

TlsSetValue

Устанавливает значение TLS для указанного индекса TLS в вызываемом потоке

TerminateThread

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

WaitForInputIdle

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

Ниже приводится пример, демонстрирующий работу с потоками с использованием стандартной функции _beginthreadex для выделения потокам отдельной TLS (Thread Local Storage) памяти, которая разделяет глобальные переменные, используемые в C/C++. Данный пример в значительной мере демонстрирует использование механизмов, описанных в данной главе.

В примере приведена программа имитации локальной архитектуры потоков клиент/сервер. В распоряжение пользователя предоставляется три серверных потока, время, данное серверным потокам на обработку запросов, возможность послать им запрос (кнопка Request) и список, в котором отображаются состояния потоков. Кроме того, реализована (скорее имитирована) очередь, в которой могут находиться не более пяти запросов пользователя, ожидающих освобождения серверных потоков. Всего очередь состоит из 3 обслуживаемых и 5 ожидаемых запросов. При поступлении ожидающих запросов будет выдано сообщение "Waiting for server thread", а при переполнении очереди - "Stack overloaded". Управление очередью осуществляет синхронизирующий объект ядра "семафор". Также в примере показана работа с критическими секциями.

Интерфейс этой программы приведен на рисунке 8.4, который поможет воспроизвести программу при реализации примера.

Таким образом, в данной главе были рассмотрены основные приемы и методы работы с потоками в среде Windows, а также рассмотрены способы разработки программ с использованием потоков.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]