- •1. Потоки. Определение. Области применения
- •2. Необходимость создания потоков
- •3. Функция потока. Создание потока
- •4. Завершение потока
- •5. Возврат управления функцией потока
- •6. Приостановка и возобновление потоков
- •7. Функция Sleep
- •8. Переключение потоков
- •9. Синхронизация потоков
- •9.1. Критические секции
- •Void InitializeCriticalSection(pcritical_section pcs);
- •Void DeleteCriticalSection(pcritical_section pcs);
- •Void EnterCriticalSection(pcritical_section pcs);
- •Void LeaveCriticalSection(pcritical_section pcs);
- •9.2. Правила использования критических секций
- •10. Порядок выполнения работы
- •11. Контрольные вопросы
3. Функция потока. Создание потока
Каждый поток начинает выполнение с некой входной функции. В первичном потоке таковой является main, wmain, WinMain или wWinMain. При создании вторичного потока, в нем тоже должна быть входная функция, которая выглядит примерно так:
DWORD WINAPI ThreadFunc(PVOID pvParam) { DWORD dwResult = 0;
return(dwResult); }
Функция потока может выполнять любые задачи. Рано или поздно она закончит свою работу и вернет управление. В этот момент поток остановится, память, отведенная под его стек, будет освобождена, а счетчик пользователей его объекта ядра «поток» уменьшится на 1. Когда счетчик обнулится, этот объект ядра будет разрушен. Но, как и объект ядра «процесс», он может жить гораздо дольше, чем сопоставленный с ним поток.
А теперь поговорим о самых важных вещах, касающихся функций потоков:
-
В отличие от входной функции первичного потока, у которой должно быть одно из четырех имен: main, wmain, WinMain или wWinMain, — функцию потока можно назвать как угодно. Однако, если в программе несколько функций потоков, необходимо присвоить им разные имена, иначе компилятор или компоновщик решит, что создается несколько реализаций единственной функции.
-
Поскольку входным функциям первичного потока передаются строковые параметры, они существуют в ANSI- и Unicode-версиях: main - wmain и WinMain - wWinMain. Но функциям потоков передается единственный параметр, смысл которого определяется разработчиком, а не операционной системой. Поэтому здесь нет проблем с ANSI/Unicode.
-
Функция потока должна возвращать значение, которое будет использоваться как код завершения потока. Здесь полная аналогия с библиотекой С/С++: код завершения первичного потока становится кодом завершения процесса.
-
Функции потоков (да и остальные функции) должны по мере возможности обходиться своими параметрами и локальными переменными. Так как, кстати, ческой или глобальной переменной могут одновременно обратиться несколько потоков, есть риск повредить ее содержимое. Однако параметры и локальные переменные создаются в стеке потока, поэтому они в гораздо меньшей степени подвержены влиянию другого потока.
Теперь рассмотрим, как заставить операционную систему создать поток, который выполнит эту функцию.
Для создания потоков в Windows предусмотрена функция:
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, // атрибуты защиты
DWORD cbStack, // размер стека потока
PTHREAD_START_ROUTINE pfnStartAddr, // адрес функции потока
PVOID pvParam, // параметр для передачи в функцию потока
DWORD fdwCreate, // флаги создания потока
PDWORD pdwThreadID); // адрес для идентификатора потока
При каждом вызове этой функции система создает объект ядра «поток». Это не сам поток, а компактная структура данных, которая используется операционной системой для управления потоком и хранит статистическую информацию о потоке. Так что объект ядра «поток» — полный аналог объекта ядра «процесс».
Система выделяет память под стек потока из адресного пространства процесса. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он получает доступ ко всем описателям объектов ядра, всей памяти и стекам всех потоков в процессе. За счет этого потоки в рамках одного процесса могут легко взаимодействовать друг с другом.
Рассмотрим параметры данной функции.
Параметр psa является указателем на структуру SECURITY_ATTRIBUTES. Значение NULL – атрибуты защиты по умолчанию. Чтобы дочерние процессы смогли наследовать описатель этого объекта, определите структуру SECURITY_ATTRIBUTES и инициализируйте ее элемент hlnheritHandle значением TRUE.
Параметр cbStack определяет, какую часть адресного пространства поток сможет использовать под свой стек. Значение 0 - CreateThread создает стек для нового потока, используя информацию, встроенную компоновщиком в ЕХЕ-файл. Смотрите ключ компоновщика -
/STACK.[reserve] [,commit]
Аргумент reserve определяет объем адресного пространства, который система должна зарезервировать под стек потока (по умолчанию — 1 Мб). Аргумент commit задает объем физической памяти, который изначально передается области, зарезервированной под стек (по умолчанию — 1 страница).
Параметр pfnStartAddr определяет адрес функции потока, с которой должен будет начать работу создаваемый поток, а параметр pvParam идентичен параметру рvРаrаm функции потока. CreateThread лишь передает этот параметр по эстафете той функции, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее значение. Оно может быть или просто числовым значением, или указателем на структуру данных с дополнительной информацией. Вполне допустимо и даже полезно создавать несколько потоков, у которых в качестве входной точки используется адрес одной и той же функции. Например, можно реализовать Web-сервер, который обрабатывает каждый клиентский запрос в отдельном потоке. При создании каждому потоку передается свое значение рvParam.
Параметр fdwCreate определяет дополнительные флаги, управляющие созданием потока. Он принимает одно из двух значений. 0 (исполнение потока начинается немедленно) или CREATE_SUSPENDED. В последнем случае система создает поток, инициализирует его и приостанавливает до последующих указаний. Флаг CREATE_SUSPENDED позволяет программе изменить какие-либо свойства потока перед тем, как он начнет выполнять код. Правда, необходимость в этом возникает довольно редко.
Последний параметр pdwThreadID функции CreateThread — это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому потоку. В Windows 2000 и Windows NT 4 в этом параметре можно передавать NULL (обычно так и делается). В Windows 95/98 это приведет к ошибке, так как функция попытается записать идентификатор потока пo нулевому адресу, что недопустимо. И поток не будет создан.
Примечание:
Если при разработке программных продуктов используется VC ++, то вместо явного вызова функции CreateThread следует производить вызов библиотечной функции _beginthreadex. При явном вызове CreateThread не происходит корректная обработка исключений и возникают утески памяти, которые возникают из-за того, что при завершение не освобождаются блоки памяти, связанные с окружением потока (подробнее см. Дж. Рихтер “Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows” ).
У функции _beginthreadex тот же список параметров, что и у CreateThread, но их имена и типы несколько отличаются (Группа, которая отвечает в Microsoft за разработку и поддержку библиотеки С/С++, считает, что библиотечные функции не должны зависеть от типов данных Wmdows). Как и CreateThread, функция _beginthreadex возвращает описатель только что созданного потока. Однако из-за некоторого расхождения в типах данных необходимо позаботиться об их приведении к тем, которые нужны функции _beginthreadex. Дж. Рихтер использует небольшой макрос chBEGINTHREADEX, который и делает всю эту работу в исходном коде.
typedef unsigned (__stdcall *PTHREAD_START) (void *);
#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, \
pvParam, fdwCreate, pdwThreadID) \
((HANDLE) _beginthreadex( \
(void *) (psa), \
(unsigned) (cbStack), \
(PTHREAD_START) (pfnStartAddr), \
(void *) (pvParam), \
(unsigned) (fdwCreate), \
(unsigned *) (pdwThreadID)))
Прототип функции _beginthreadex:
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (*start_address)(void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr);