Потоки (Threads) и многопоточность в Windows
C++, WinAPIСодержание
Intro
ОС MS Windows 95 представила программерам идею использования многозадачной (multitasking) операционной системы (даже несмотря на то, что в действительности она таковой не являлась, т.к. использовала т.н. "вытесняющую" (preemptive) многозадачность, при которой маленькие порции множества программ выполняются, можно сказать, одновременно, в порядке одной общей очереди).1 Суть идеи заключалась в том, что на одном компьютере могло одновременно выполняться несколько процессов (приложений), каждому из которых поочерёдно выделяются определённые порции процессорного времени, называемые временные срезы (time slices).Многозадачность также даёт возможность делить каждый процесс на несколько отдельных процессов, называемых потоками (threads). У каждого потока есть своё предназначение, например: сканирование сетевой активности, поддержка пользовательского ввода, проигрывание звуков и т.д. Использование в одном приложении более чем одного потока называется многопоточностью (multithreading).
Создаём поток
Создание отдельного потока внутри приложения - задача совсем несложная. Для создания потока ты создаёшь функцию (используя специальный прототип функции), которая содержит код, который необходимо выполнить в данном отдельном потоке. Прототип функции создания потока выглядит так:DWORD WINAPI ThreadProc(LPVOID lpParameter);
Здесь аргумент lpParameter - это заданный программистом указатель, который затем необходимо будет указать при создании потока.
Создаём поток путём вызова функции CreateThread:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAbilities, // NULL DWORD dwStackSize, // 0 LPTHREAD_START_ROUTINE lpStartAddress, // thread function LPVOID lpParameter, // Указатель, предоставляемый программистом. Может быть NULL DWORD dwCreationFlags, // 0 LPDWORD lpThreadId // Получает идентификатор потока );
Обрати внимание
Возвращаемое значение функции CreateThread - это его дескриптор, который также обязательно должен быть закрыт при завершении работы потока. В противном случае ресурсы не будут освобождены. Ресурсы, занятые созданным потоком, освобождаются путём вызова функции CloseHandle:
CloseHandle(hThread); // используй дескриптор, полученный при создании потока функцией CreateThread
Следующий пример создаёт в приложении отдельный поток и инициализирует его:
// Какая-либо функция создания отдельного потока DWORD WINAPI MyThread(LPVOID lpParameter) { BOOL *Active; Active = (BOOL*)lpParameter; *Active = TRUE; // Помечаем поток как активный // Вставляем исходный код здесь // Уничтожаем поток *Active = FALSE; // Помечаем поток как неактивный ExitThread(O); // Специальный вызов функции для закрытия потока } void InitThread() { HANDLE hThread; DWORD ThreadId; BOOL Active; // Созадём поток, передавая определённую программистом переменную, которая // используется для сохранения статуса потока. hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThread, (void*)&Active, 0, &ThreadId); // Ожидаем завершения работы потока, путём постоянного отслеживания состояния флага активности while(Active == TRUE); // Закрываем дескриптор потока CloseHandle(hThread); }
В данном примере в приложении создаётся отдельный поток, который начинает свою работу сразу после выполнения функции CreateThread. Во время вызова данной функции мы передаём указатель в переменную типа BOOL, которая отслеживает состояние потока (активен или нет), путём установки значений в TRUE или FALSE. По завершении работы потока, выставляем флаг активности в FALSE, указывая, что поток более неактивен. Далее уничтожаем поток путём вызова функции ExitThread, имеющей всего один параметр - код уничтожения потока (termination code) (грубо говоря, причина, по которой закрывается данный поток). Обычно здесь просто указывают 0 (ноль).
По сути, поток - это обычная функция, которая выполняется параллельно со своим приложением.
Критические секции (Critical sections)
Так как все современные ОС семейства MS Windows являются многозадачными (multitasking), рано или поздно Windows-приложения начинают сбоить при обращении к общим ресурсам, особенно те, которые используют несколько потоков. Представь ситуацию, когда один поток заполняет структуру какими-либо очень важными данными, а другой вдруг пытается получить доступ к ним или даже пытается их изменить.Существует способ, позволяющий быть уверенным, что только один выбранный поток имеет полный контроль над данными. Именно для этого был придуман механизм критических секций (областей) (critical sections).2 При активации критическая секция блокирует попытки всех остальных потоков получить доступ к разделяемой памяти (shared memory; общая память, которую используют все потоки). Это позволяет каждому отдельному потоку индивидуально получать доступ или изменять данные приложения, не опасаясь, что другие потоки смогут вмешаться в данный процесс.
Для использования критической секции её сперва необходимо объявить и инициализировать:
CRITICAL_SECTION CriticalSection; InitializeCriticalSection(&CriticalSection);
Сразу после этого можно войти в критическую секцию, обработать данные и выйти из неё, как это представлено ниже:
EnterCriticalSection(&CriticalSection); // Обрабатываем важные данные здесь LeaveCriticalSection(&CriticalSection);
По окончании работы с критической секцией (например при выходе из приложения), освобождаем её:
DeleteCriticalSection(&CriticalSection);
Применять критические секции совсем не трудно, но, в то же время, крайне необходимо при создании многопоточных приложений. Главное правило здесь код, расположенный в области критической секции, должен выполняться как можно быстрее. В противном случае ты рискуешь подвесить системные процессы ОС, что может привести к её "падению".
Источники
1. Young V. "Programming a multiplayer FPS in DirectX", Charles River Media Inc., 2005
2. Adams J. "Programming Role Playing Games with DirectX, 2nd Ed.", Premier Press, 2004
Последние изменения страницы Вторник 21 / Июнь, 2022 19:10:15 MSK
Последние комментарии wiki