Поток исполнения программы — это способ добиться параллельности ее работы. Если говорится, что программа содержит два потока, значит, существует два набора инструкций, выполняющихся параллельно или псевдопараллельно. Почему псевдопараллельно? Потому что в системе с одним процессором просто невозможно одновременно выполнять две команды. В таких случаях обычно используется переключение между потоками, т.е. операционная система сама делит по своим внутренним предпочтениям процессорное время на запущенные потоки. С широким использованием многоядерных или многопроцессорных систем стало еще важнее распараллеливать потоки исполнения программы.
Одним из самых распространенных вариантов применения этой технологии является «скидывание» в отдельный поток какой-нибудь достаточно продолжительной операции, для того чтобы иметь возможность корректно реагировать на события пользовательского интерфейса в главном потоке программы. Если вы запустили какую-либо операцию в программе, а она не реагирует на ваши действия, то скорее всего в данном случае существует только один главный поток.
Использование потоков в программировании — огромный пласт как теоретического, так и практического знания. В этот раз мы остановимся на применении потоков в C++ и инкапсуляции потоков в классе.
К сожалению, невозможно без дополнительных ухищрений использовать в качестве функции потока функции—члены класса. Во-первых, обычно для функции, содержащей тело потока, задана вполне конкретная сигнатура, которую программист не может изменить. Вторым камнем преткновения является то, что при вызове функции—члена в C++ используется неявный параметр — указатель на экземпляр класса.
На самом деле никто не запрещает применять простую функцию в стиле Си. Однако такой подход лишит нас прозрачности и легкости понимания кода.
Первый способ, который можно придумать, — использование статической функции — члена класса с явным параметром, ссылкой на объект этого класса. Такая функция будет «заглушкой» для вызова «настоящей» функции-члена после явного преобразования параметра к объекту класса.
class PThreadedAdapter
{
protected:
HANDLE workerThread;
bool workerLive, isThreadRunning;
static DWORD WINAPI ThreadStub(LPVOID
lParameter) {
((PThreadedAdapter*)lParameter)->ThreadProc();
return 0;
}
virtual void ThreadProc(void)=0;
}
//Поток создается следующим образом:
::CreateThread(NULL,0,(LPTHREAD_START_
ROUTINE)PThreadedAdapter::ThreadStub, this, 0,
&dwWorkerID);
Функция—член класса ThreadProc должна будет содержать в себе то, что поток будет выполнять, т.е. для применения этого «трюка» требуется создать новый класс, сделать описанный PThreadedAdapter родительским для него и реализовать чисто виртуальную функцию ThreadProc. В функции ThreadProc нужно установить «места прерывания» и проверять в них переменную isRunning. Если isRunning становится false, то необходимо корректно завершить работу и установить workerLive в false. Это сообщит родительскому потоку, что потоковая функция успешно закончила свою работу.
Для удобства добавим еще функции Start и Stop, которые, как видно из их имен, будут запускать и останавливать поток, встроенный в наш класс.
void PThreadedAdapter::Start(void) {
workerLive = true;
// Если будет false, то поток сразу остановится
DWORD dwWorkerID;
workerThread = ::CreateThread(NULL,0,
(LPTHREAD_START_ROUTINE
PThreadedAdapter::ThreadStub, this, 0,
&dwWorkerID);
if(!workerThread) { // Запустить не удалось
workerLive = false; isRunning = false;
}
}
void PThreadedAdapter::Stop() {
workerLive = false;
while(isRunning) {
Sleep(waitTime);
}
}
За рамками этой небольшой статьи осталось очень многое из мира потоков. Для самостоятельной работы предлагаю вам реализовать шаблонный класс-обертку для потокового класса или рассмотреть, как устроены и используются потоки в библиотеке Boost.