Потоки (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 thread2(LPVOID t) { /* Код второго потока */ return 0; }?1!Функция потока должна соответствовать следующему прототипу:
!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 << "First thread\n"; HANDLE thread = CreateThread(NULL,0,thread2,NULL, 0, NULL); cout << "More data from first thread\n"; for (int i = 0; i < 1000000; i++) {} cout << "Even more data from first thread\n"; _getch(); return 0; } DWORD WINAPI thread2(LPVOID t) { cout << "Second thread\n"; return 0; }?1!У меня программа выводит в консоль следующее:

Сначала выводится текст First thread. Далее создаётся второй поток. А вот что выведется дальше однозначно утверждать нельзя: More data from first thread или Second thread. Зависит от того какой поток первым успеет напечатать сообщение. В большинстве случаев первый поток успеет быстрее, всё-таки второму потоку ещё предстоит выделить ресурсы и начать выполнять свою функцию.
Дальше в первом потоке я запускаю пустой цикл, выполняющийся миллион раз. За это время второй поток успевает напечатать сообщение Second thread. И после этого выводится последнее сообщение первого потока.
Заключение
В дальнейших уроках я буду постепенно вводить многопоточные приложения, что позволит нам рассмотреть особенности работы с ними.
Надеюсь, основная идея работы с потоками вам понятна.
P.S. В прикреплённом архиве находится .exe файл релизной конфигурации. У меня эта версия выводит сообщение второго потока самым последним - компилятор успешно оптимизирует пустой цикл for.
Комментарии