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

четверг, 29 января 2009 г.

Миллисекундный таймер для Windows и UNIX

Очень часто в программе удобно иметь возможность засекать и мерить интервалы времени. Стандартная функция time() конечно хороша своей переносимостью, но она работает с секундами, а хочется что-то более быстрое. Микросекунды - это уже тоже перебор. А вот миллисекунды - самое оно.
Итак, задача: сделать простой и переносимый класс C++ для работы с интервалами времени в миллисекундах. Должно работать в Windows и UNIX.
Я придумал вот такой интерфейс для класса:
class PreciseTimer {
public:
// Тип для работы с тиками таймера. По сути это целое в 64 бита,
// но конкретное имя рабочего типа будет зависеть от платформы.
typedef s_int_64 Counter;

// Функция получение текущего значения миллисекундного таймера.
// Само по себе это число особого смысла не имеет, так как оно
// ни к чему не привязано, а вот разница двух таких чисел как
// раз используется для замеров интервалов времени.
// Функция возвращает 0 под Windows, если не удается получить
// значение системного таймера.
Counter millisec();

// Задержка на указанное число миллисекунд. Необходимо учитывать,
// что в UNIX системах данная функция может быть прервана
// системым сигналом (signal). В этом случае задержка может быть
// меньше, чем ожидалось.
static void sleepMs(int ms);

// Функция "отметки" текущего момента времени.
// Добавляет текущее время в очередь отметок.
void mark();

// Функция получения времени, прошедшего с последней отметки
// в функции mark(). Последняя отметка вынимается из очереди
// и вычитается из текущего значения таймера. Эта разница и
// есть результат функции. Если очередь отметок пуста (никто
// не вызывал mark() до этого), то возвращается -1.
Counter release();

// Парные вызовы mark()/release() могут быть вложенными.
//
// Примерная техника работы с классом:
// ...
// PreTimer timer;
// ...
// timer.mark();
// ...что-то делаем тут (1)
// timer.mark();
// ...что-то еще делаем тут (2)
// /* получаем продолжительность дела (2) */
// t1 = timer.release();
// /* получаем суммарную продолжительность дел (1) и (2) */
// t2 = timer.release();
// /* А t3 уже равно -1, так как очередь пуста, так как этот
// * вызов release() третий в счету, а вызовов mark() было
// * всего два */
// t3 = timer.release();
}
Реалиазация вышла довольно простая. Всего один файл pretimer.h (без .cpp) без внешних нестандартных зависимостей.

Файл pretimer.h:
#ifndef _EXT_PRETIMER_H
#define _EXT_PRETIMER_H

#include <stack>

#ifdef WIN32
#include <windows.h>
#else
#include <sys/time.h>
#include <unistd.h> // usleep()
#endif

// namespace, традиционно, с именем "ext", так что измените под ваши
// привычки именования, если надо.
namespace ext {

class PreciseTimer {
public:
#ifdef WIN32
// Тип int64 для Windows
typedef LONGLONG Counter;
#else
// Тип int64 для UNIX
typedef long long Counter;
#endif
PreciseTimer();

Counter millisec();

void mark();
Counter release();

static void sleepMs(int ms);
private:
// Тип стека для хранения отметок времени.
typedef std::stack< Counter > Counters;

// Стек для хранения отметок времени.
Counters __counters;

#ifdef WIN32
// Для Windows надо хранить системную частоту таймера.
LARGE_INTEGER __freq;
#endif
};

void PreciseTimer::mark() {
__counters.push(millisec());
}

PreciseTimer::Counter PreciseTimer::release() {
if( __counters.empty() ) return -1;
Counter val = millisec() - __counters.top();
__counters.pop();
return val;
}

#ifdef WIN32

PreciseTimer::PreciseTimer() {
// Для Windows в конструкторе получаем системную частоту таймера
// (количество тиков в секунду).
if (!QueryPerformanceFrequency(&__freq))
__freq.QuadPart = 0;
}

PreciseTimer::Counter PreciseTimer::millisec() {
LARGE_INTEGER current;
if (__freq.QuadPart == 0 || !QueryPerformanceCounter(¤t))
return 0;
// Пересчитываем количество системных тиков в миллисекунды.
return current.QuadPart / (__freq.QuadPart / 1000);
}

void PreciseTimer::sleepMs(int ms) {
Sleep(ms);
}

#else // WIN32

PreciseTimer::PreciseTimer() {}

PreciseTimer::Counter PreciseTimer::millisec() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

void PreciseTimer::sleepMs(int ms) {
usleep(ms * 1000);
}

#endif // WIN32

} // ext

#endif // _EXT_PRETIMER_H
Итак, класс готов, но надо попробовать его в работе. Я, как сугубый апологет unit-тестирования, напишу тесты. Для их компиляции вам потребуется библиотека Google Test Framework. Вы можете взять оригинал с официального сайта, а можете для простоты воспользоваться моей версией, упакованной в два компактных файла gtest-all.cc и gtest.h. Я уже писал про это в рассказе про unit-тестирование. Там я подробно описал, как подготовить Google Test к удобной работе.

Итак, тесты.

Файл pretimer_unittest.cpp:
#include "gtest/gtest.h"
#include <cstdlib>

// Подключаем наш класс
#include "pretimer.h"

// Простой тест, для Windows, в основном, для проверки
// доступности системного таймера.
TEST(PreciseTimer, PreciseSystemTimerAvailability) {
ext::PreciseTimer timer;
// Если метод millisec() возвращает 0, значит недоступен
// системный таймер.
EXPECT_NE(0, timer.millisec()) << "Недоступен системный таймер";
}

// Тестирует "точность" измерений.
TEST(PreciseTimer, MeasurementAccuracy) {
// Тестируем на задержке в 100 миллисекунд.
const int delay_ms = 100;
// Зададим наше допустимое отклонение в 10% (10 миллисекунд).
// Функция задержки msleep() тоже неидеальна и привносит
// какую-то погрешность помимо наших измерений.
const int allowed_delta_ms = 10;
// Создаем таймер
ext::PreciseTimer timer;
// Замечаем время
timer.mark();
// Ждем 100 миллисекунд
msleep(delay_ms);
// Вычисляем модуль разницы между эталоном в 100 миллисекунд
// и измеренным нами интервалом через mark()/release()
int delta = std::abs(static_cast<int>(delay_ms - timer.release()));

// Если отклонение более 10 миллисекунд - ошибка.
EXPECT_TRUE(delta <= allowed_delta_ms)
<< "Слишком большое отклонение " << delta << ", превышающее " << allowed_delta_ms;
}

// Тестируем очередь замеров
TEST(PreciseTimer, Queue) {
// Создаем таймер
ext::PreciseTimer timer;
// Делаем замер номер 1
timer.mark();
// Делаем замер номер 2
timer.mark();
// Получаем текущее значение таймера
ext::PreciseTimer::Counter a = timer.release();
// Ждем 100 миллисекунд
monitor::PreciseTimer::sleepMs(100);
// Проверяем, что значение таймера до задежки
// меньше, чем после. Этим мы проверили, что
// очередь замеров работает, так как получили
// корректное значение второго в очереди замера.
EXPECT_LT(a, timer.release());
}

// Проверка пустой очередь замеров
TEST(PreciseTimer, EmptyQueue) {
ext::PreciseTimer timer;
// Если очередь замеров пуста, метод release() должен
// возвращать -1.
EXPECT_EQ(-1, timer.release());
}
Я потратил на этот класс часа четыре неторопливой работы, а на написание тестов всего полчаса, но эти полчаса будут мне служить верой и правдой еще очень долго.
Забавно, что когда я запускал эти тесты как-то на Windows под виртуальной машиной, то тест MeasurementAccuracy давал сбой! Видимо виртуальная машина как-то неправильно эмулировала работу таймеров, и замер делался совершенно неправильно. А вот теперь если представить - как бы я искал этот баг вручную по всей боевой программе, а? Кто ж мог предположить, что в виртуальной среде что-то можно пойти не так с таймерами.
Снова повторю - unit тестирование forever!
В завершении, нам нужна главная программа для запуска тестов:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
// Инициализируем библиотеку
testing::InitGoogleTest(&argc, argv);
// Запускаем все тесты, прилинкованные к проекту
return RUN_ALL_TESTS();
}
Компилируем:

Visual Studio:
cl /EHsc /I. /DWIN32 /Fepretimer_unittest.exe runner.cpp pretimer_unittest.cpp gtest-all.cc
UNIX:
g++ -I. -o pretimer_unittest runner.cpp pretimer_unittest.cpp gtest-all.cc
Запускаем pretimer_unittest и получаем:
[==========] Running 3 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 4 tests from PreciseTimer
[ RUN ] PreciseTimer.PreciseSystemTimerAvailability
[ OK ] PreciseTimer.PreciseSystemTimerAvailability
[ RUN ] PreciseTimer.MeasurementAccuracy
[ OK ] PreciseTimer.MeasurementAccuracy
[ RUN ] PreciseTimer.Queue
[ OK ] PreciseTimer.Queue
[ RUN ] PreciseTimer.EmptyQueue
[ OK ] PreciseTimer.EmptyQueue
[----------] Global test environment tear-down
[==========] 4 tests from 1 test case ran.
[ PASSED ] 4 tests.
Ура! Все работает. Доказано тестами. При использовании данного класса у себя в проекте не забудьте добавить pretimer_unittest.cpp в набор ваших прочих unit тестов. Этим вы избавитесь от множества сюрпризов.

Приобщайтесь к unit-тестированию, и программируйте правильно!


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

Скрипты для Visual Studio

По роду работы у меня на компьютере стоят сразу несколько версий Visual Stdio: 2003, 2005 и 2008. В целом они легко уживаются на одной машине, и при работе в графической оболочке обычно не возникает каких-либо неудобств или конфликтов. Но вот при работе через командную строку обычно надо как-то понимать, какой именно компилятор хочется вызвать (ведь имя то у него одно - cl.exe, a версий три). В итоге я убрал из путей PATH все ссылки на каталоги разных версии студии, и сделал вот такие скрипты, помещенные в любой каталог, находящийся в списке путей PATH.

Visual Studio 2003, файл: cl2003.cmd:
@"%VS71COMNTOOLS%\vsvars32.bat"
Visual Studio 2005, файл: cl2005.cmd:
@"%VS80COMNTOOLS%\vsvars32.bat"
Visual Studio 2008, файл: cl2008.cmd:
@"%VS90COMNTOOLS%\vsvars32.bat"
Если вы ставили студии по умолчанию стандартным образом, то в системе должны быть переменные окружения VS71COMNTOOLS, VS80COMNTOOLS и VS90COMNTOOLS, задающие расположение конкретной версии. Скрипт же vsvars32.bat поставляется вместе со студией и автоматически настраивает все необходимое для компилятора окружение.

Теперь компиляция в версии 2005 делается, например, вот таким cmd-файлом:
call cl2005.cmd
cl /Fetest.exe test.cpp
Очевидно, что для перехода на версию 2003 или 2008 надо заменить всего одну цифру. Очень удобно.

среда, 28 января 2009 г.

Unit-тестирование для параллельных потоков

В статье про класс Thread, реализующий потоки в С++, я обещал как минимум показать, как работать с данным классом, и как максимум рассказать про блочное (unit) тестирование в целом, и про его применение для проверки работы нашего класса в частности.
Дожив до четвертого десятка и имея за спиной десяток с хвостиком, посвященный программированию, к своему огромному стыду к программированию с использованием блочного тестирования (TDD - test driven development) я приобщился только год назад. Честно могу сказать - это было для меня одним из сильнейших потрясений в профессиональной области за последнее время, и радикально поменяло некоторые фундаментальные представления о разработке софта. Как прирожденный максималист в профессии (за что часто очень нелюбим коллегами по цеху, которые руководствуются правилом "лучшее враг хорошего"), я работаю под девизом "мои программы должны быть безупречны". А так как тут мне дали в руки такой волшебный инструмент как блочное тестирование, я стараюсь теперь его применять где только возможно. Даже порой радикально перерабатывая старые проекты.
Ладно, это лирика. Приступим к делу.

У нас есть класс Thread, расположенный в файлах thread.cpp и thread.h.

Напишем небольшой пример.

Файл thread_example.cpp:
#include <iostream>
#include "thread.h"

// Создаем наследника от класса Thread
class MyThread: public ext::Thread {
public:
// Инициализируем в false флаг завершения в конструкторе
MyThread() : __done(false) {}
virtual void Execute() {
// В процессе работы потока меняем флаг завершения на истину
__done = true;
}
// Функция, возвращающая значение флага завершение
bool done() const { return __done; }
private:
bool __done;
};

int main(int argc, char* argv[]) {
// Создаем объект потока. Пока он еще не запущен.
MyThread thread;
// Печатаем значение флага завершения. Должно быть 0 (false)
std::cout << "Thread status before: " << thread.done() << std::endl;
// Запускаем поток
thread.Start();
// И ждем его завершения
thread.Join();
// Если поток нормально был запущен и отработал, то значение
// флага должно измениться на 1 (true). Это должна сделать
// функция Execute(). Если тут будет не 1, а 0, значит поток
// не выполнялся, и выходит, что с классом что-то не так.
std::cout << "Thread status after: " << thread.done() << std::endl;
}
Компилируем (естественно, из командной строки).

Visual Studio 2008 (хотя подойдет любая версия VS):
cl /EHsc /I. /Fethread_example /DWIN32 thread_example.cpp thread.cpp
Опция "/EHsc" нужна, так как мы пишем на С++, и поэтому компилятору cl.exe надо явно указать необходимость включения поддержки исключений. Особенность данного компилятора.
Если вы в UNIX'e, тогда, например, gcc:
g++ -o thread_example thread_example.cpp thread.cpp
Запускаем thread_example, и имеем на экране следующее:
Thread status before: 0
Thread status after: 1
Судя по напечатанным данным, класс работает правильно.
Я специально не использовал в функции Execute() отладочной печати на экран типа "Hello, world! I'm the thread". Хотя это было бы нагляднее и прикольнее, чем какие-то булевы флаги. Но на это была причина. При работе с потоками, когда ваш код теперь уже выполняется нелинейно, а какие-то фрагменты могут работать параллельно, приходится очень тщательно продумывать совместное использование переменных одновременно работающими потоками. Может так случиться, что когда основной поток будет печатать что-то на экран через переменную std::cout, параллельный поток тоже захочет это сделать, прервет основной поток на полпути и сам начнет использовать std::cout. Данные обоих потоков смешаются, и в лучшем случае на экран вылезет каша, а в худшем программа может завершиться аварийно. На том же мной так любимом AIX'е именно это и происходит. Видимо, стандартная библиотека AIX'а требует каких-то дополнительных настроек для нормальной работы в мультипотоковой среде. Для избежания подобных проблем совместного доступа применяются различные механизмы из мира параллельного программирования - блокировки (mutex), семафоры, критические секции и т.д. Я посвящу отдельный пост этому очень непростому вопросу, но расскажу о нем крайне просто и понятно.
Теперь давайте запустим десяток потоков.

Файл thread_example2.cpp:
#include <vector>
#include <iostream>
#include "thread.h"

class MyThread: public ext::Thread {
public:
MyThread(int id) : __id(id), __done(false) {}
virtual void Execute() {
// Небольшая "перчинка" программы, чтобы не было скучно.
// Суть в том, что поток с индексом 3 (по счету номер 4, так первый
// индекс 0) не будет устанавливать флаг выполнения. Сделано это
// просто для разнообразия. Результат данной "перчинки" будет виден
// при печати.
if (__id != 3)
__done = true;
}
bool done() const { return __done; }
private:
int __id;
bool __done;
};

typedef std::vector<MyThread*> Threads;

int main(int argc, char* argv[]) {
// Создаем вектор из указателей на потоки
std::vector<MyThread*> threads;

// Создаем 10 потоков и сохраняем указатели на них в вектор
for (int i = 0; i < 10; i++)
threads.push_back(new MyThread(i));

// Запускаем потоки на выполнение
for (Threads::iterator i = threads.begin(); i != threads.end(); i++)
(*i)->Start();

// Дожидаемся, пока они все завершатся
for (Threads::iterator i = threads.begin(); i != threads.end(); i++)
(*i)->Join();

// Печатаем статусы потоков в одну строку через пробел
for (Threads::iterator i = threads.begin(); i != threads.end(); i++)
std::cout << (*i)->done() << " ";
std::cout << std::endl;

// Чистим за собой память.
for (Threads::iterator i = threads.begin(); i != threads.end(); i++)
delete *i;
}
Компилируем.

Visual Studio:
cl /EHsc /I. /Fethread_example2 /DWIN32 thread_example2.cpp thread.cpp
В UNIX'e (gcc):
g++ -o thread_example2 thread_example2.cpp thread.cpp
Запускаем thread_example2, и имеем на экране следующее:
1 1 1 0 1 1 1 1 1 1
Видно, что все потоки, кроме четвертого (индекс 3, так как считаем от нуля) установили свои флаги правильно. Четвертому помешала "перчинка" (см. выше).

Что дальше? Да ничего, собственно. Теперь вы наверняка набросаете несколько своих примеров, поиграетесь, и может начнете включать данный класс в свои проекты. Тестовые примеры вы скорее всего сотрете как отработанный материал, а может и заначите до лучших времен.

А теперь! На сцену приглашается unit тестирование.

Я вам предлагаю сделать небольшие программы-тесты, которые бы своими результатами доказывали правильность работы нашего класса. Например:
class SimpleThread: public ext::Thread {
public:
SimpleThread() : __done(false) {}

virtual void Execute() {
__done = true;
}

bool done() const { return __done; }
private:
bool __done;
};
Класс SimpleThread очень похож на класс MyThread из наших примеров выше. Он просто меняет флаг активности с false на true в процессе успешного выполнения.
 // Декларируем тест с именем RunningInParallel в группе тестов ThreadTest.
TEST(ThreadTest, RunningInParallel) {
// Создаем объект нашего класса
SimpleThread thread;
// Внимание! Макрос EXPECT_FALSE смотрит, какое значение у его аргумента.
// Если это ложь, то все нормально, и выполнение теста идет дальше. Если же нет,
// то печатается сообщение об ошибке, хотя тест продолжает работу.
// В нашем случае тут должно быть false по смыслу.
EXPECT_FALSE(thread.done());
// Запускаем поток на выполнение
thread.Start();
// Ждем завершение потока
thread.Join();
// Макрос EXPECT_TRUE смотрит, какое значение у его аргумента.
// Если это истина, то все нормально, и выполнение теста идет дальше. Если же нет,
// то печатается сообщение об ошибке, хотя тест продолжает работу.
// Тут мы уже ждем не false, а true, потому что поток должен был изменить значение
// этого флага.
EXPECT_TRUE(thread.done());
}
Теперь осознаем произошедшее - мы не просто написали какой-то пример, а мы формально опередили логику работы класса, задали его ответственность. Теперь наши пожелания к функциональности класса заданы не на словах и предположениях, а в виде программы.

Теперь осталось только запустить этот тест.
Существует много библиотек для unit тестирования практически для каждого языка. С++ не исключение. Самой распространенной в мире С++ является CppUnit. Но около полугода назад Google ворвался в мир библиотек тестирования с Google Test Framework. На момент написания данной статьи актуальной версией является 1.2.1. Распространяется в исходных текстах. Данную библиотеку можно прекомпилировать и использовать как двоичный модуль при линковке, но я сделал иначе. Так как я постоянно прыгаю с платформы на платформу, с компилятора на компилятор, мне удобнее компилировать Google Test прямо из исходников каждый раз при сборке проекта, благо библиотека хорошо портируема, мала по размеру и быстро компилируется. К небольшому сожалению, Google Test реализована в виде не одного файла-исходника и одного .h файла, а целого набора .h файлов и набора .cc (.cpp) файлов. Так удобно библиотеку развивать (что логично), но не использовать из исходников со стороны. Поэтому я объединил всю библиотеку в два файла: gtest.h и gtest-all.cc, и больше ничего не нужно. Гугловцы обещали в следующий релиз библиотеки включить мой патч на эту тему. Сейчас же они (также по моей идее) дают специальный скрипт, которым можно из официального архива сделать компактную версию из двух файлов. Для тех, у кого уже съехали мозги от этих подробностей, и кто пока не хочет тратить время на техдетали библиотеки, я могу предложить мою сборку Google Test'а. Можно начать с нее. Она основана на официальной версии 1.2.1 и является объединением множества файлов в два. В архиве два файла gtest/gtest.h и gtest-all.cc. Положите их в каталог, где будете проводить опыты.
Итак, предположим, вы имеете файлы: gtest/gtest.h и gtest-all.cc в рабочем каталоге, и все готово к запуску.

Полный вариант исходника thread_unittest.cpp:
#include "gtest/gtest.h"
#include "thread.h"

class SimpleThread: public ext::Thread {
public:
SimpleThread() : __done(false) {}

virtual void Execute() {
__done = true;
}

bool done() const { return __done; }
private:
bool __done;
};

TEST(ThreadTest, RunningInParallel) {
SimpleThread thread;
EXPECT_FALSE(thread.done());
thread.Start();
thread.Join();
EXPECT_TRUE(thread.done());
}
Я предпочитаю давать имена файлам с тестами, используя суффикс "_unittest" к имени основного файла. Это позволяет, быстро взглянув на каталог, понять - какие классы имеют тесты, а какие нет.

Также нам нужен стартовый файл, который будет содержать функцию main():

Файл runner.cpp:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
// Инициализируем библиотеку
testing::InitGoogleTest(&argc, argv);
// Запускаем все тесты, прилинкованные к проекту
return RUN_ALL_TESTS();
}
Тут все просто. Обычно, этот файл одинаков для все тестовых проектов, если вам не надо проводить какие-нибудь дополнительные инициализации, брать что-то из командной строки и.д. Google Test устроена так (в отличие от CppUnit, например), что тесты (TEST и TEST_F) не надо нигде дополнительно регистрировать, объявлять и т.д. Вы просто задаете тело теста, включаете файл с исходником в проект и все. Далее все происходит автоматически.
Резонный вопрос - а в каком порядке тесты буду выполнены, если их несколько? А ответ прост: вас это не касается. Тесты могут выполняться в любом порядке, и нельзя делать никаких предположений на эту тему. Суть тут в том, что каждый тест должет быть атомарным и независимым (конечным автоматом без памяти). В этом суть блочного (unit) тестирования, когда маленькие кусочки большой программы проверяются отдельно, в полной изоляции. Но, вернемся к компиляции.
Компилируем.

Visual Studio:
cl /EHsc /DWIN32 /I. /Fethread_unittest.exe runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc

Запускаем thread_unittest и получаем что-то вроде:
[==========] Running 1 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 1 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[----------] Global test environment tear-down
[==========] 1 tests from 1 test case ran.
[ PASSED ] 1 tests.
Это значит, что тест был запущен и отработал как положено.

Добавим еще один тест, который будет проверять, убивается ли поток, когда мы этого ходим.

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

#ifdef WIN32
#include <windows.h>
#define msleep(x) Sleep(x)
#else
#include <unistd.h>
#define msleep(x) usleep((x)*1000)
#endif

class SimpleThread: public ext::Thread {
public:
SimpleThread() : __done(false) {}

virtual void Execute() {
__done = true;
}

bool done() const { return __done; }
private:
bool __done;
};

TEST(ThreadTest, RunningInParallel) {
SimpleThread thread;
EXPECT_FALSE(thread.done());
thread.Start();
thread.Join();
EXPECT_TRUE(thread.done());
}

// "Нескончаемый поток"
class GreedyThread: public ext::Thread {
public:
virtual void Execute() {
// Данный поток будет работать вечно, пока его не убьют извне.
while (true) {
msleep(1);
}
}
};

TEST(ThreadTest, Kill) {
// Создаем "вечный" поток
GreedyThread thread;
// Запускаем его
thread.Start();
// Убиваем его
thread.Kill();
// Если функция Kill() не работает, ты мы никогда не дождемся окончания потока
// и программа тут повиснет.
thread.Join();
}
Компилируем.

Visual Studio:
cl /EHsc /I. /Fethread_unittest.exe /DWIN32 runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
UNIX:
g++ -I. -o thread_unittest runner.cpp thread_unittest.cpp thread.cpp gtest-all.cc
Запускаем thread_unittest и получает что-то вроде:
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
[ OK ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 2 tests.
Оба теста отработали правильно. Получается, что теперь мы точно уверены, что наш поток умеет работать параллельно и независимо от основного потока, и умеет принудительно "убиваться" по требованию. Мы это доказали тестами, а не словами или алгоритмами на бумаге. Если вам кажется, что еще не вся функциональность класса проверена, обязательно допишите свои тесты для проверки своих предположений.

Теперь внесем в класс "случайную ошибку", добавив оператор "return" в виндовый вариант функции "void Thread::Start()":
void Thread::Start() {
// "Случайная" ошибка
return;
__handle = CreateThread(
0, 0,
reinterpret_cast<LPTHREAD_START_ROUTINE>(ThreadCallback), this,
0, 0
);
}
Теперь наш класс "сломан". Посмотрим, что скажет тестирование (естественно, надо перекомпилировать программу перед этим):
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel

1 FAILED TEST
Бинго! Тест говорит, что ожидаемое значение флага выполнения "истина", а реальное "ложь". Класс не работает! Конечно не работает, так как создание потока не происходит из-за "случайного" оператора "return". Мы нашли реальный "баг", причем сделали это автоматизированным образом.

Можно еще улучшить тест дополнительной информацией, которая будет показана в случае его сбоя:
TEST(ThreadTest, Simple) {
SimpleThread thread;
EXPECT_FALSE(thread.done());
thread.Start();
thread.Join();
EXPECT_TRUE(thread.done()) << "Поток не изменил флаг";
}
Теперь сообщение об ошибке будет более информативно.
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from ThreadTest
[ RUN ] ThreadTest.RunningInParallel
thread_unittest.cpp(33): error: Value of: thread.done()
Actual: false
Expected: true
Поток не изменил флаг
[ FAILED ] ThreadTest.RunningInParallel
[ RUN ] ThreadTest.Kill
[ OK ] ThreadTest.Kill
[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran.
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] ThreadTest.RunningInParallel

1 FAILED TEST
Google Test имеет множество функций для тестовых сравнений, но основные их них, используемые в 99% случаев, следующие:
  • EXPECT_EQ(a, b) - проверка условия "a = b"
  • EXPECT_NE(a, b) - проверка условия "a != b"
  • EXPECT_GT(a, b) - проверка условия "a > b"
  • EXPECT_LT(a, b) - проверка условия "a < b"
  • EXPECT_GE(a, b) - проверка условия "a >= b"
  • EXPECT_LE(a, b) - проверка условия "a <= b"
  • EXPECT_TRUE(a) - проверка аргумента на истину
  • EXPECT_FALSE(a) - проверка аргумента на ложь
Функции, начинающиеся с EXPECT_, в случае ошибки не прерывают выполнение теста, а просто печатают сообщение об ошибке, и тестирование продолжается. Если ваша ошибка фатальна (например, база данных недоступна), и нет причин продолжать тесты вообще, то можно использовать функции со схожим именованием:
  • ASSERT_EQ(a, b) - проверка условия "a = b"
  • ASSERT_NE(a, b) - проверка условия "a != b"
  • ASSERT_GT(a, b) - проверка условия "a > b"
  • ASSERT_LT(a, b) - проверка условия "a < b"
  • ASSERT_GE(a, b) - проверка условия "a >= b"
  • ASSERT_LE(a, b) - проверка условия "a <= b"
  • ASSERT_TRUE(a) - проверка аргумента на истину
  • ASSERT_FALSE(a) - проверка аргумента на ложь
Эти фунции при ошибке прерывают тест и весь процесс тестирования с целом.

Есть еще особая функция FAIL(), которая безусловно прерывает тест с ошибкой. Удобно для проверки мест, где вы "не должны" оказаться в процесса работы теста. Например:
try {
...
} catch(...) {
FAIL() << "Данный кусок программы не должен генерировать исключений";
}

Полный список функций-проверок, а также описания прочих возможностей Google Test, так как я затронул пока лишь малую их часть, можно получить в документации.

Кроме того, во все эти функции можно писать как стандартные потоки вывода через оператор "<<", как мы делали в примере выше:
EXPECT_TRUE(thread.done()) << "Поток не изменил флаг";
тем самым печатая удобную отладочную информацию.
Давайте проанализируем сказанное и сделанное. Что мы получили? Как я уже говорил, мы формализовали наши требования от класса в виде программы, которую можно теперь запускать сколько угодно раз, проверяя работу класса. Вы спросите для чего? Класс-то работает. А вот представьте, что вы установили новую версию компилятора или новую версию библиотеки pthread и что-то в этом роде. Вы уверены, что в них нет ошибок? или может нужны другие опции командной строки для правильной работы. Кто знает?! Тест знает! Скомпилированный и запущенный тест сразу же проверит, работает ли класс так, как вы от него ожидаете. По крайне мере хуже уже не будет. Новые ошибки тест может и не покажет, но уже формализованное ранее поведение класса проверит точно. А теперь представьте, что вам надо так перепроверить сотни классов в вашем проекте. Только автоматизированное тестирование делает это реальным. А тестирование типа "давай поерзаем программой быстренько, и если сразу не сломалось, то все хорошо" тестированием не является вообще. Гораздо проще включить компилирование и запуск тестов при каждой полной сборке проекта. Небольшая потеря времени конечно есть на дополнительную компиляцию, но это с лихвой окупается выявленными тут же ошибками. Сами unit тесты обычно работают очень быстро. Они должны быть быстрыми, иначе они неудобны для регулярного запуска. Сотни тестов не должны как-либо заметно медленно работать. Если какой-то тест требует секунд для себя, то может его стоит перенести в раздел функционального тестирования и пользоваться им уже в процессе проверки программы для релиза, а не в процессе самой разработки, или запускать медленные тесты автоматически в ночных сборках.

Кстати, наличие тестов позволяет поручить возможные доработки кода не только тому, кто этот код писал изначально и понимает в самых деталях, как все работает. Если тесты работают, значит изменения кода по крайне мере не сделали его хуже, а значит клиент не будет кричать сразу после установки новой версии типа "какого вы тут все сломали". Тесты - это прежде доказательства программиста, что его программа работает так, как он ожидает и всем обещает, как его программа должна работать. Только это уже не просто слова, а автоматизированный метод проверки.

Помните те примеры, которые мы писали в начале. Что с ними случилось? Мы их просто выкинули как отработанный материал. Выкинули результаты очень полезной работы. Мы по кусочкам разобрались, как работает исследуемый класс, но потом отказались повторно использовать уже полученные результаты, выкинув тестовые примеры. Так почему бы изначально не приложить чуть-чуть усилий и не оформить тестовые игрушечные примеры в виде блочных тестов, готовых к автоматизированному повторному использованию, и не превратить их в мощное автоматизированное оружие против багов?
Личный пример. Писал я класс, реализующий TCP/IP сокет с использованием SSL. Скачал библиотеку OpenSSL, начал разбираться. Стал писать мини примеры для освоения разных аспектов библиотеки. И каждый свой эксперимент я оформлял в виде теста (один тест для создания контекста ключей, другой для установления соединения, третий для расшифрации кодов ошибок и т.д.). Каждый новый запуск проекта влючал все больше и больше таких тестов. Затем я вынужден был прерваться на месяц. По прошествии месяца я напрочь забыл все про OpenSSL. И если бы не готовые уже тесты, я бы начал разбираться опять сначала. А так, поглядев на уже сделанные куски, я быстро погонял тесты, вспомнил что к чему, и продолжил работу. Затем из этих тестов фактически и родилась моя библиотека для работы с SSL, и сами тесты включились в тестирующую сборку. Когда осваиваешь что-то новое - язык, библиотеку и т.д. - тестовая программа очень быстро разрастается и превращается некоего монстра, в котором вы хотите задействовать и проверить все новое. Гораздо полезнее разбираться по маленьким кусочкам, изолированно изучать каждый вопрос, закрепляя полученные результаты в виде тестов.

Вы меня сходу спросите - а как писать тесты? Ведь данный пример весьма тривиален, а реальные программы гораздо сложнее, в них много взаимозависимостей, и порой крайне сложно раскроить их на тестируемые кусочки. Ответ, который я дам сходу сейчас таков - пишите ваши программы сразу пригодными для тестирования. А вот как именно это делать - я расскажу в будущих выпусках нашего научно-популярного журнала.

А вы меня опять спросите - а другие языки как? как, например, делать unit тестирование в классическом языке С? Об этом я тоже непременно расскажу.

Unit-тестирование — это громадная и очень интересная тема. Будем ее развивать.

P.S. Исходные тексты данной статьи я проверял на Windows, Linux 2.6 (32- и 64-бит Intel и SPARC), AIX 5.3 и 6, SunOS 5.2 64-bit SPARC.


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

вторник, 27 января 2009 г.

Защита объектов от случайного копирования в С++

Внимательный читатель наверняка заметил в посте про реализацию параллельных потоков на С++ следующий фрагмент кода:
class Thread {
public:
...
private:
...
// Защита от случайного копирования объекта в C++
Thread(const Thread&);
void operator=(const Thread&);
};
Это определения конструктора копирования и перегруженого оператора присваивания. Причем непосредственно реализаций этих функций нигде нет, только определения. Вопрос: для чего все это?
Давайте разберемся с назначением этих функций. Их прямая задача уметь копировать объект данного класса. А что произойдет, если вы по какой-то причине не определили конструктор копирования или оператор присваивания (может просто забыли), а пользователь вашего класса решил скопировать объект, возможно даже неосознанно? В этом случае компилятор сам определит конструктор копирования по умолчанию, который будет тупо копировать объект байт за байтом без учета смысла копируемых данных. И вам крупно повезет, если все члены-данные вашего класса являются либо базовыми типами (int, long, char и т.д.), либо имеют корректные конструкторы копирования и операторы присваивания. В этом случае все будет хорошо — базовые типы компилятор умеет копировать правильно, а сложные типы скопируют себя сами через их конструкторы копирования. А представьте, что вы внутри своего класса создаете объекты динамически в куче и храните в классе только указатели на них. Указатеть — это базовый тип, и компилятор его нормально скопирует. А вот данные, на которые этот указатель указывает он копировать не будет. В итоге два объекта (старый-оригинал и новый-копия) будут ссылаться на один кусок памяти в куче. Теперь понятно, что при попытке освобождения этой памяти в деструкторе (если вы не забыли этого сделать ;-) кто-то из этих двух объектов попытается освободить уже освобожденную память. Вероятность аварийного завершения программы в этом случае крайне высока, а поиск подобных ошибок может быть крайне долгим и мучительным.
Отсюда мораль: если для вашего класса не заданы конструктор копирования и оператор присваивания (они вам не нужны по смыслу), сделайте им пустые объявления в разделе закрытом разделе (private). Тогда попытка скопировать этот объект сразу приведет к ошибке при компиляции. Во-первых, объявления являются закрытыми (private), и сторонний пользователь вашего класса получит ошибку доступа к закрытым данным класса. Во-вторых, у этих функций нет тел, а значит вы сами не выстрелите себе в ногу, попытавшись случайно скопировать объект данного класса в нем же самом (тут вам private уже не помеха), если вы на это не рассчитывали при проектирование класса.

Лично я делаю так. У меня есть следующий файл:

ctorguard.h:
#ifndef _EXT_CTOR_GUARD_H
#define _EXT_CTOR_GUARD_H
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
#endif
Теперь определение класса будет выглядеть так:
#include "ctorguard.h"
class Thread {
public:
...
private:
...
// Защита от случайного копирования объекта в C++
DISALLOW_COPY_AND_ASSIGN(Thread);
};
Теперь вы надежно предохранены.
Кстати, вдогонку. При реализации оператора присваивания надо обязательно проверять — не пытаетесь ли вы копировать объект сам в себя, то есть, не является ли источник копирования самими объектом куда идет копирование. Если это произойдет, вы легко можете получить переполнения стека как самый вероятный исход из-за бесконечного вызова оператора присваивания себя самим.

понедельник, 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.


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

суббота, 24 января 2009 г.

Вступительное слово, или "Почему? собственно"

Прекрасно сказано:
Математик любит искать во всем логику, закономерности, разумность. Если ее не хватает в реальной жизни, то компьютер, операционная система, языки программирования дополняют этот дефицит, служат своего рода отдушиной, тем сказочным миром, который помогает легче переносить уродливость мира реального. Следовательно, чем более иррациональным будет повседневное бытие, тем больше будет тяга к компьютеру, к его удивительно логичному и разумному поведению, осмысленным действиям, внутренней логике, виртуальной действительности. Там нет тупых и невежественных генералов, очередей за мясом, совхозов и овощебаз, общественной работы и субботников, там только четкие и понятные критерии, TRUE и FALSE, единица и ноль. А специалист по компьютерам, хороший программист всегда будет востребован, при любом правителе, любой идеологии, любых начальниках. Отсюда уже недалеко и до свободы, до реальной свободы, когда пропадает этот инстинктивный трепет перед важными надутыми начальниками, не освоившими толком даже компьютерных игр.

Михаил Масленников
Криптография и свобода

Это, наверное, первый и последний нетехнический пост в данном блоге. Далее все будет носить исключительно технический характер про компьютеры, программирование, роботов и так далее со всеми остановками. Изначальная идея была просто сделать сборник моих собственных фишек и штучек, изобретенных или подсмотренных. Многие вещи быстро забываются, и хочется иметь онлайновый справочник. Кроме того, идеи глупо держать под подушкой, там они гниют и тухнут. Идеями надо делиться, что я и планирую делать тут.

Будет интересно. Следите за анонсами.