Содержание статьи
Сегодня практически все ОС поддерживают многозадачность. Потоки, процессы и прочие атрибуты сейчас уже не кажутся чем-то страшным и необычным. Мультипоточность используется практически в любом современном приложении. А еще в наше время очень активно применяется ООП. Сегодня мы узнаем, как в C++ подружить между собой и многопоточность, и ООП.
Перед нами стоит задача написать класс, который будет запускать некоторый код в отдельном треде. Для этого будем использовать стандартную функцию Windows API — CreateThread. Более подходящим решением было бы применить _beginthread из стандартной библиотеки, но для отражения общей сути CreateThread вполне сгодится. Давай взглянем на код.
Создание потока из класса
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
// тут мы не можем получить доступ к
// закрытым членам класса
return 0;
}
class MyClass
{
public:
MyClass(void);
~MyClass(void);
void RunThread();
private:
int m_intVar;
};
void MyClass::RunThread()
{
HANDLE hThread;
DWORD idThread;
hThread = ::CreateThread(NULL, 0, &ThreadFunc,
0, 0, &idThread);
}
Все очень просто. Есть класс MyClass, который создает поток с помощью CreateThread. Потоковая функция ThreadFunc находится вне класса. Компилируется этот код без проблем, но есть несколько нюансов, из-за которых такое решение может оказаться неприемлемым.
Основная проблема заключается в том, что мы не можем получить доступ к внутренним методам и переменным класса. Первое, что приходит в голову для решения этой задачки — сделать потоковую функцию членом класса. То есть примерно так:
Потоковая функция — член класса
class MyClass
{
public:
...
void RunThread();
private:
DWORD WINAPI ThreadFunc(LPVOID lpParam);
int m_intVar;
};
DWORD WINAPI MyClass::ThreadFunc(LPVOID lpParam)
{
...
return 0;
}
void MyClass::RunThread()
{
HANDLE hThread;
DWORD idThread;
// на такой код будет ругаться компилятор
hThread = ::CreateThread(NULL, 0, &ThreadFunc,
0, 0, &idThread);
}
Но если попробовать скормить этот код компилятору, то мы получим ошибку, суть которой заключается в том, что тип потоковой функции не соответствует требуемому. Если копнуть глубже, то мы поймем, что компилятор просто не может понять, адрес какой именно ThreadFunc передавать в качестве третьего параметра в CreateThread. При этом объектов MyClass может быть создано несколько, и располагаться они могут в любом куске памяти, а API для создания треда требует указания точного адреса потоковой функции, который, понятное дело, становится известен лишь во время выполнения программы. Так что компилятор лишь разводит руками и предлагает еще немного пораскинуть мозгами.
Следующее, что приходит в голову, это опять вынести ThreadFunc за пределы класса, но в этот раз сделать ее дружественной к MyClass с помощью директивы friend. Объявленная таким образом потоковая функция должна без проблем получить доступ к private членам MyClass. Но, так как ThreadFunc существует в единственном экземпляре, а объектов класса может быть множество, то нам придется передавать потоковой функции в качестве параметра указатель на создающий тред объект.
Потоковая функция, дружественная классу
DWORD WINAPI ThreadFunc(LPVOID lpParam);
class MyClass
{
public:
...
void RunThread();
friend DWORD WINAPI ThreadFunc(LPVOID lpParam);
private:
int m_intVar;
};
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
// а тут мы уже можем обращаться к private членам
MyClass* mc = (MyClass*)lpParam;
mc->m_intVar = 90;
cout << _T("Start thread, m_intVar = ")
<< mc->m_intVar;
return 0;
}
void MyClass::RunThread()
{
HANDLE hThread;
DWORD idThread;
// передаем потоковой функции указатель на
// текущий объект
hThread = ::CreateThread(NULL, 0, &ThreadFunc,
this, 0, &idThread);
}
Все в этом примере хорошо, за исключением одного: к ThreadFunc можно обратиться в обход MyClass, а это не только делает бессмысленным создание отдельного класса для проектирования потока, но и нарушает инкапсуляцию — основной принцип объектноориентированного программирования. Потоковая функция имеет доступ к закрытым членам класса, а следовательно, любой желающий может их изменить, нарушая все законы ООП. Очевидно, что для решения проблемы с инкапсуляцией нам надо вернуть ThreadFunc обратно в пределы класса. Но, как мы уже видели выше, компилятор отказывается работать с таким кодом.
Исправить это можно, объявив потоковую функцию статической. В этом случае ее адрес будет известен на этапе сборки программы, что позволит избежать проблем с компиляцией.
Но так как функция-член ThreadFunc статическая, то, работая с другими членами MyClass, она не сможет корректно определить, с каким именно объектом класса ей нужно производить те или иные действия. Как и в случае с дружественной функцией, нам надо будет передавать потоку в качестве параметра указатель на текущий объект.
Статическая потоковая функция — член класса
class MyClass
{
public:
...
void RunThread();
private:
static DWORD WINAPI ThreadFunc(LPVOID lpParam);
int m_intVar;
};
DWORD WINAPI MyClass::ThreadFunc(LPVOID lpParam)
{
// и тут мы уже можем обращаться к private членам
MyClass* mc = (MyClass*)lpParam;
mc->m_intVar = 90;
cout << _T("Start thread, m_intVar = ")
<< mc->m_intVar;
return 0;
}
void MyClass::RunThread()
{
HANDLE hThread;
DWORD idThread;
// передаем потоковой функции указатель
// на текущий объект
hThread = ::CreateThread(NULL, 0, &ThreadFunc,
0, 0, &idThread);
}
Теперь мы решили все наши проблемы. Внеся код треда в MyClass, мы обошли сложности с инкапсуляцией — теперь доступ к закрытым членам класса осуществляется только лишь из метода класса. А объявив ThreadFunc в private блоке, мы тем самым лишили пользователей нашего класса возможности вызвать потоковую функцию напрямую.
Этот вариант решения нашей задачки можно назвать самым оптимальным и, казалось бы, остановиться на этом. Но есть еще один трюк, в основном для любителей C++ Builder, о котором я должен рассказать.
Реализация компилятора от Борланда содержит в себе директиву __closure, которая служит для определения типа обработчика события. Вообще, обработчик события представляет собой указатель на функцию. Обычно этот указатель имеет 4-байтный размер, но при определении такого типа указателя функции передается еще и указатель this на экземпляр класса, поэтому указатель имеет 8-байтовый размер.
Воспользовавшись этой директивой и немного подправив код, потоковую функцию уже можно не делать статической. Для большей наглядности посмотрим код:
Использование __closure
typedef unsigned long (__stdcall *ThdFunc)(void
*arg); // прототип функции потока
typedef unsigned long (__closure *ClassMethod)(void
*arg); // прототип метода класса
// данное объединение позволяет решить несостыковку с типами
typedef union
{
ThrdFunc Function;
ClassMethod Method;
} tThrdAddr;
class MyClass
{
private:
tThrdAddr Addr;
protected:
unsigned long ThreadFunc(void *arg)
{
...
};
public:
RunThread()
{
DWORD idThread;
Addr.Method = &ThrdHandle;
// тут идет преобразование указателя
CreateThread(NULL, 0, Addr.Function,
this, 0, &idThread);
};
};
Конечно, такое решение пригодно лишь для использования его в среде Builder, а многие программисты считают его грязным хаком. К тому же, использование объединений для решения проблем с типизацией — то еще извращение. Но, с другой стороны, если код работает корректно — значит, он имеет право на существование.
Заключение
Теперь мы можем писать ООП-код, который выполняется в различных потоках. Перечисленные выше способы не являются единственными — возможно, кто-то придумал что-то более интересное для реализации данной задачи. Но для написания качественных программ этого вполне достаточно.