*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

понедельник, 26 января 2009 г.

Универсальные потоки на С++ для Windows и UNIX

Потоки (threads) являются весьма удобным механизмом для ускорения программ и придания им гибкости в использовании процессорного времени, особенно в наш успешно и бесповоротно наступивший век многоядерных процессоров, стоящих почти в каждом современном компьютере. Чего уж говорить о серверных платформах.

Итак, задался я целью иметь удобный и простой класс на С++ для работы с потоками. В силу особенностей работы мне приходится иметь дело различными системами, и хотелось иметь максимально переносимый вариант. На сегодняшний день стандартом де-факто для мира UNIX являются так называемые потоки POSIX (pthreads). Для Windows тоже есть реализация этой библиотеки, но в целях исключения дополнительной внешней зависимости для этой платформы я решил пользоваться напрямую Windows API, благо назначения функций очень похожи. При использования POSIX Threads под Windows данный класс еще упрощается (надо просто выкинуть всю Windows секцию), но для меня лично удобнее было не иметь зависимости от виндусовых POSIX Threads. Дополнительная гибкость, так сказать.

Исходники приведены прямо тут, благо они небольшие. Комментариев мало, так как я считаю, что лучший комментарий, это грамотно написанный код. Сердце всего дизайна класса — это виртуальный метод "void Execute()", который и реализует работу потока. Данный метод должен быть определен в вашем классе потока, который наследуется от класса Thread.
Я всегда использую пространства имен (namespaces) в C++, особенно для библиотечных классов общего назначения. Для данного примера я использовал имя "ext". Замените его на ваше, если необходимо "вписать" класс в ваш проект.
Для компиляции в Windows необходимо определить макрос WIN32. В этом случае будет использоваться Windows API. Иначе подразумевается работа с pthreads. Если вы используете Cygwin, то можно работать и через Windows API и через pthreads.

Файл thread.h:
#ifndef _EXT_THREAD_H
#define _EXT_THREAD_H

#ifdef WIN32
#include <windows.h>
#else
#include <pthread.h>
#include <signal.h>
#endif

namespace ext {

#ifdef WIN32
typedef HANDLE ThreadType;
#else
typedef pthread_t ThreadType;
#endif

class Thread {
public:
Thread() {}
virtual ~Thread();

// Функция запуска потока. Ее нельзя совместить с конструктором
// класса, так как может случиться, что поток запустится до того,
// как объект будет полностью сформирован. А это может спокойно
// произойти, если вызвать pthread_create или CreateThread в
// в конструкторе. А вызов виртуальной функции в конструкторе,
// да еще и в конструкторе недосформированного объекта,
// в лучшем случае приведет к фатальной ошибке вызова чисто
// виртуальной функции, либо в худшем случае падению программы
// с нарушением защиты памяти. Запуск же потока после работы
// конструктора избавляет от этих проблем.
void Start();

// Главная функция потока, реализующая работу потока.
// Поток завершается, когда эта функция заканчивает работу.
// Крайне рекомендуется ловить ВСЕ исключения в данной функции
// через try-catch(...). Возникновение неловимого никем
// исключения приведет к молчаливому падению программы без
// возможности объяснить причину.
virtual void Execute() = 0;

// Присоединение к потоку.
// Данная функция вернет управление только когда поток
// завершит работу. Применяется при синхронизации потоков,
// если надо отследить завершение потока.
void Join();

// Уничтожение потока.
// Принудительно уничтожает поток извне. Данный способ
// завершения потока является крайне нерекомендуемым.
// Правильнее завершать поток логически, предусмотрев
// в функции Execute() условие для выхода, так самым
// обеспечив потоку нормальное завершение.
void Kill();

private:
ThreadType __handle;

// Защита от случайного копирования объекта в C++
Thread(const Thread&);
void operator=(const Thread&);
};

} // ext

#endif


Файл thread.cpp:
#include "thread.h"

namespace ext {

static void ThreadCallback(Thread* who) {
#ifndef WIN32
// Далаем поток "убиваемым" через pthread_cancel.
int old_thread_type;
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_thread_type);
#endif
who->Execute();
}

#ifdef WIN32

Thread::~Thread() {
CloseHandle(__handle);
}

void Thread::Start() {
__handle = CreateThread(
0, 0,
reinterpret_cast<LPTHREAD_START_ROUTINE>(ThreadCallback), this,
0, 0
);
}

void Thread::Join() {
WaitForSingleObject(__handle, INFINITE);
}

void Thread::Kill() {
TerminateThread(__handle, 0);
}

#else

Thread::~Thread() {
}

extern "C"
typedef void *(*pthread_callback)(void *);

void Thread::Start() {
pthread_create(
&__handle, 0,
reinterpret_cast<pthread_callback>(ThreadCallback),
this
);
}

void Thread::Join() {
pthread_join(__handle, 0);
}

void Thread::Kill() {
pthread_cancel(__handle);
}

#endif

} // ext

Возникает резонный вопрос — я почему ни один из вызовов функций не проверяет код ошибки. Вдруг что? Я могу сказать, что я встретил только один случай возврата ошибки от pthread_create.
Это было на AIX'e при использовании связывания (linking) времени исполнения. Программа не была слинкована с библиотекой pthreads (я забыл указать ключик "-lpthread"), но из-за особенностей линковки времени исполнения (так любимой AIX'ом) линкер сообщил, что все хорошо и выдал мне исполняемый файл. В процессе же работы ни одна функция из библиотеки pthreads просто не вызывалась. Интересно, что код ошибки функции pthread_create() означал что-то типа "не могу открыть файл", и чего я сделал вывод, что файл библиотеки недоступен. Вообще, линковка времени исполнения — это довольно хитрая штука. В данном виде связывания внешние связи определены уже на стадии линковки (то есть это не тоже самое, что загрузка разделяемой библиотеки вручную во время работы, самостоятельный поиск функций по именам и т.д.), но вот фактический поиск вызываемой функции происходит в сам момент старта программы. Получается, что до непосредственно запуска нельзя проверить в порядке ли внешние зависимости (команда ldd рапортует, что все хорошо). Более того, разрешение внешних зависимостей происходить в момент вызовы внешней функции. Это довольно гибкий механизм, но вот его практическая полезность пока остается для меня загадкой. Вообще AIX является довольно изощренной системой в плане разнообразия механизмов связывания. Позже я постараюсь описать результаты моих "исследований" AIXа на эту тему.
Но вернемся к причинам отсутствия проверки кодов возврата от функций pthreads и Windows API. Как я уже упомянул, если какая-то из этих функций завешается с ошибкой, то с огромной вероятностью что-то радикально не так в системе, и это не просто нормальное завершение функции с ошибкой, после которой можно как-то работать дальше. Это фатальная ошибка, и ваше приложение не будет работать нормально еще по туче других причин. Кроме этого я хотел сделать это класс максимально простым, чтобы его можно было таскать из проекта в проект и не допиливать его напильником под существующую в проекте систему обработки ошибок (исключения, коды возврата, журналирование и т.д.), так как в каждом проекте она может быть разная.

Читатель всегда может добавить в код необходимые проверки для собственных нужд.

Кроме этого, я всегда использую в разработке unit-тестирование, и данный класс также имеет тесты. Поэтому запускаемый при каждой полной сборке проекта набор тестов сразу выявляет большинство проблем (уже проблемы линковки точно).

В следующей главе я расскажу про технику использования описанного класса — как создавать потоки, как их запускать, останавливать и уничтожать. Я буду использовать unit-тестирование, что позволит все мини-примеры превращать в автоматизированные тесты вашего проекта.

В завершении могу сказать, что данный класс успешно работает и проверен мной лично на Windows (32- и 64-бит), Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC, HP-UX и HP-UX IA64.


Другие посты по теме:

9 комментариев:

  1. Да очень полезный класс. Ну а насчет ошибок наверное достаточно assert на проверку handle, и кодов возврата.

    ОтветитьУдалить
  2. Да, assert'ов тут вполне должно хватить вместо какой-либо логической обработки ошибок.

    ОтветитьУдалить
  3. Кстати следует заметить что для работы с CRT лучше создавать поток через _begihthread/_beginthreadeex вместо CreateThread, согласно MSDN.

    ОтветитьУдалить
  4. Вопрос спорный. Лиично я во множестве библиотек видел использование CreateThread, а не _beginthread. Обычно для C++ люди пишут wrapper типа как я сделал тут. И CreateThread тут в самый раз. А роль _beginthread вообще непонятна. Вроде какая-то совместимость с другими компиляторами, то вот не понятно с какими. Разве что с Борландом.

    ОтветитьУдалить
  5. Подскажите пожалуйста, можно ли если потоковая функция Thread::Execute() отработала заново запустить её через Thread::Start() ?
    Заранее спасибо.

    ОтветитьУдалить
  6. Александр: Если поток уже отработал, то да.

    ОтветитьУдалить
  7. _beginthread и _beginthreadex инициализаруют CRT для потока, а CreateThread - нет. Это нехорошо, если используются функции из библиотеки языка C. Вроде бы, тот же errno в случае CreateThread окажется не локальным для потока, а глобальным для всех потоков.
    И еще, если я правильно помню, то, в случае, когда POSIX-поток завершится без вызова pthread_join(), то его ресурсы (стек и тп) не освободятся и все равно после нужно вызвать pthread_join().

    ОтветитьУдалить
  8. А как в Posix Threads с поддержкой стандартной С библиотеки(errno, strtok etc) ?

    ОтветитьУдалить