Потоки (threads) в WinAPI
Когда приложение начинает свою работу, для него создаётся процесс (process). Обычно, каждой программе соответствует один процесс.
При создании процесса для него выделяется память — виртуальное адресное пространство (virtual address space). Когда в отладчике мы смотрим на адреса переменных — мы видим адреса из этого пространства.
Потоки (Threads)
Каждый процесс имеет как минимум один поток (thread). До сих пор наши программы состояли из одного процесса и одного потока. В этом уроке мы научимся создавать дополнительные потоки в процессе.
Самый главный вопрос о потоках: для чего нужно несколько потоков? Разные потоки могут одновременно выполняться разными ядрами процессора. Например, в нашей однопоточной программе одна функция просчитывает искусственный интеллект, а другая — физику взаимодействия объектов. В этом случае сначала будет выполнена одна функция, а потом вторая. Если же мы разделим программу на два процесса и запустим программу на двухъядерном процессоре, то искусственный интеллект и физика будут просчитываться одновременно.
Все потоки имеют доступ к адресному пространству процесса. И это может стать серьёзной проблемой. Например, один поток обрабатывает данные и, одновременно, второй пытается вывести эти же данные на экран. Что произойдёт? Успеет ли первый поток обработать все данные, до того как до них доберётся второй поток? Или же второй поток обгонит первый, и часть данных пользователь увидит обработанными, а часть — нет? Эти вопросы мы обсудим в следующих уроках.
С точки зрения C++ поток — это обычная функция имеющая определённый прототип. Для создания потока используется функция CreateThread.
Создание потоков — CreateThread
Функция CreateThread возвращает описатель потока:
!1?HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );?1!
1. lpThreadAttributes — данный аргумент определяет, может ли создаваемый поток быть унаследован дочерним процессом. Мы не будем создавать дочерние процессы, поэтому ставим NULL.
2. dwStackSize — размер стека в байтах. Если передать 0, то будет использоваться значение по-умолчанию (1 мегабайт).
3. lpStartAddress — адрес функции, которая будет выполняться потоком. Т.е. можно сказать, что функция, адрес которой передаётся в этот аргумент, является создаваемым потоком. Данная функция должна соответствовать определённому прототипу — рассмотрим ниже. Имя функции может быть любым — вы сами его выбираете.
4. lpParameter — указатель на переменную, которая будет передана в поток.
5. dwCreationFlags — флаги создания. Здесь можно отложить запуск выполнения потока. Мы будем запускать поток сразу же, передаём 0.
6. lpThreadId — указатель на переменную, куда будет сохранён идентификатор потока. Нам идентификатор не нужен, передаём NULL.
Давайте посмотрим на код вызова CreateThread:
!1?HANDLE thread = CreateThread(NULL,0,thread2,NULL, 0, NULL);?1!
Здесь мы сохраняем описатель потока в переменной thread. Обратите внимание на третий аргумент — адрес функции потока. thread2 — имя функции, которая и будет являться вторым потоком. Вот её код:
Функция потока должна соответствовать следующему прототипу:
!1?DWORD WINAPI ThreadProc(LPVOID lpParameter)?1!
Аргумент, который может принимать данная функция передаётся четвёртым параметров функции CreateThread. Если отбросить все переопределения типов, то данный прототип выглядит так:
!1?unsigned long __stdcall ThreadProc(void* lpParameter)?1!
Напоследок рассмотрим пример создания второго потока:
Создание программы с двумя потоками
Код программы можно скачать в начале урока. Это простая консольная программа. Для работы с потоками необходимо включить файл windows.h. Рассмотрим основной код:
!1?DWORD WINAPI thread2(LPVOID); int main() < cout
Creating Threads
The CreateThread function creates a new thread for a process. The creating thread must specify the starting address of the code that the new thread is to execute. Typically, the starting address is the name of a function defined in the program code (for more information, see ThreadProc). This function takes a single parameter and returns a DWORD value. A process can have multiple threads simultaneously executing the same function.
The following is a simple example that demonstrates how to create a new thread that executes the locally defined function, MyThreadFunction .
The calling thread uses the WaitForMultipleObjects function to persist until all worker threads have terminated. The calling thread blocks while it is waiting; to continue processing, a calling thread would use WaitForSingleObject and wait for each worker thread to signal its wait object. Note that if you were to close the handle to a worker thread before it terminated, this does not terminate the worker thread. However, the handle will be unavailable for use in subsequent function calls.
The MyThreadFunction function avoids the use of the C run-time library (CRT), as many of its functions are not thread-safe, particularly if you are not using the multithreaded CRT. If you would like to use the CRT in a ThreadProc function, use the _beginthreadex function instead.
It is risky to pass the address of a local variable if the creating thread exits before the new thread, because the pointer becomes invalid. Instead, either pass a pointer to dynamically allocated memory or make the creating thread wait for the new thread to terminate. Data can also be passed from the creating thread to the new thread using global variables. With global variables, it is usually necessary to synchronize access by multiple threads. For more information about synchronization, see Synchronizing Execution of Multiple Threads.
The creating thread can use the arguments to CreateThread to specify the following:
- The security attributes for the handle to the new thread. These security attributes include an inheritance flag that determines whether the handle can be inherited by child processes. The security attributes also include a security descriptor, which the system uses to perform access checks on all subsequent uses of the thread’s handle before access is granted.
- The initial stack size of the new thread. The thread’s stack is allocated automatically in the memory space of the process; the system increases the stack as needed and frees it when the thread terminates. For more information, see Thread Stack Size.
- A creation flag that enables you to create the thread in a suspended state. When suspended, the thread does not run until the ResumeThread function is called.
You can also create a thread by calling the CreateRemoteThread function. This function is used by debugger processes to create a thread that runs in the address space of the process being debugged.
Create thread windows с
Функция CreateThread создает поток, который выполняется в пределах виртуального адресного пространства вызывающего процесса.
Чтобы создавать поток, который запускается в виртуальном адресном пространстве другого процесса, используется функция CreateRemoteThread .
LPSECURITY_ATTRIBUTES lpThreadAttributes, // дескриптор защиты
SIZE_T dwStackSize, // начальный размер стека
LPTHREAD_START_ROUTINE lpStartAddress, // функция потока
LPVOID lpParameter, // параметр потока
DWORD dwCreationFlags, // опции создания
LPDWORD lpThreadId // идентификатор потока
[in] Указатель на структуру SECURITY_ATTRIBUTES , которая обуславливает, может ли возвращенный дескриптор быть унаследован дочерними процессами. Если lpThreadAttributes является значением ПУСТО (NULL), дескриптор не может быть унаследован.
Windows NT/2000/XP: член структуры lpSecurityDescriptor определяет дескриптор безопасности для нового потока. Если lpThreadAttributes имеет значение ПУСТО (NULL), поток получает заданный по умолчанию дескриптор защиты. Списки контроля доступа ( ACL ) в заданном по умолчанию дескрипторе безопасности для потока поступают из первичного маркера или маркера заимствования прав создателя.
[in] Начальный размер стека, в байтах. Система округляет это значение до самой близкой страницы памяти. Если это значение нулевое, новый поток использует по умолчанию размер стека исполняемой программы. Дополнительную информацию см. в статье Размер стека потока
Обратите внимание! на то, что, в случае необходимости, размер стека растет.
[in] Указатель на определяемую программой функцию типа LPTHREAD_START_ROUTINE , код которой исполняется потоком и обозначает начальный адрес потока. Для получения дополнительной информации о функции потока, см. ThreadProc .
[in] Указатель на переменную, которая передается в поток.
[in] Флажки, которые управляют созданием потока. Если установлен флажок CREATE_SUSPENDED , создается поток в состоянии ожидания и не запускается до тех пор, пока не будет вызвана функция ResumeThread . Если это значение нулевое, поток запускается немедленно после создания. В это время, никакие другие значения не поддерживаются.
Windows XP: Если установлен флажок STACK_SIZE_PARAM_IS_A_RESERVATION , параметр dwStackSize задает начальный резервный размер стека. Иначе, dwStackSize устанавливает фиксированный размер.
[out] Указатель на переменную, которая принимает идентификатор потока.
Windows NT /2000/XP: Если этот параметр имеет значение ПУСТО (NULL), идентификатор потока не возвращается.
Windows 95/98/Me: Этот параметр не может быть значением ПУСТО (NULL).
Если функция завершается успешно, величина возвращаемого значения — дескриптор нового потока.
Если функция завершается с ошибкой, величина возвращаемого значения — ПУСТО (NULL). Чтобы получать дополнительные данные об ошибках, вызовите GetLastError .
Обратите внимание! , что функция CreateThread может завершиться успешно, даже если lpStartAddress указывает на данные, код или не понятно куда. Если ее начальный адрес ошибочен, когда поток запускается, происходит исключительная ситуация и поток заканчивает работу. Завершение работы потока в результате недопустимого начального адреса обрабатывается как выход из-за ошибки для процесса потока. Это поведение похоже на асинхронный характер CreateProcess , где процесс создается, даже если он адресуется к ошибочным или отсутствующим библиотекам динамической связи ( DLL ).
Windows 95/98/Me: функция CreateThread завершается успешно, только тогда, когда она вызвана в контексте 32-разрядной программы. 32-разрядная DLL не может создать дополнительный поток, когда эта DLL вызывается 16-разрядной программой.
Число потоков, которые процесс может создать, ограничено доступной виртуальной памятью. По умолчанию, каждый поток имеет один мегабайт пространства стека. Поэтому, Вы можете создавать самое большое 2028 потоков. Если Вы преобразовываете заданный по умолчанию размер стека, Вы можете создавать большее количество потоков. Однако ваше приложение будет иметь лучшую эффективность, если Вы создаете один поток на процессор и выстраиваете очередь запросов, которые приложение сохраняют информацию контекста. Поток должен обрабатывать все запросы в очереди перед обработкой запросов следующей очереди.
Новый дескриптор потока создается с правами доступа THREAD_ALL_ACCESS . Если дескриптор безопасности не предоставляется, этот дескриптор может быть использован в любой функции, которая требует дескриптора объекта потока. Когда дескриптор безопасности предоставлен, проверка доступа выполняется для всех последующих использованиях дескриптора прежде, чем этот доступ предоставляется. Если проверка доступа не запрещает доступ, запрашивающий процесс не может использовать дескриптор, чтобы получить доступ к потоку. Если поток исполняет роль клиента, то он вызывает CreateThread с дескриптором безопасности имеющим значение ПУСТО (NULL), созданный объект потока имеет заданный по умолчанию дескриптор безопасности, который позволяет доступ только владельцу маркера заимствования прав или членам TokenDefaultDacl. Для получения дополнительной информации, см. статью Защита потока и права доступа .
Выполнение потока начинается в функции, указанной параметром lpStartAddress . Если эта функция возвращает значение, величина возвращаемого значения ДВОЙНОЕ СЛОВО (DWORD) используется, чтобы завершить работу потока неявным вызовом функции ExitThread . Чтобы получить величину возвращаемого значения потока, используйте функцию GetExitCodeThread .
Поток создается с приоритетом потока THREAD_PRIORITY_NORMAL . Используйте функции GetThreadPriority и SetThreadPriority , чтобы получать и установить приоритетное значение потока.
Когда поток заканчивает работу, объект потока приобретает сигнального состояния, удовлетворяя любые потоки, которые ждали объект.
Объект потока остается в системе, до тех пор, пока не поток закончит работу, и все дескрипторы к нему не будут закрыты через вызов CloseHandle .
Функции ExitProcess , ExitThread , CreateThread , CreateRemoteThread и процесс, который запускается (как результат вызова CreateProcess), в пределах процесса переводятся между собой в последовательный режим. Одновременно в адресном пространстве может происходить только одно из этих событий. Это означает нижеследующие ограничения выполнения:
|
Поток, который использует функции из библиотек этапа исполнения C, для управления потоком должен использовать C — функции этапа исполнения beginthread и endthread , а не CreateThread и ExitThread . Когда вызывается ExitThread , происходит утечка ресурсов в маленькой памяти, что приводит к сбою в программе.