*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком библиотека. Показать все сообщения
Показаны сообщения с ярлыком библиотека. Показать все сообщения

вторник, 8 марта 2011 г.

Скрипты на Lua в С++, версия 2

В прошлом году я писал мою микро библиотеку для встраивания Lua как скриптового языка в программы на С++.

С тех пор библиотека практически не изменилась. Она устраивала меня, а сторонних пользователей у нее особо не было.

Недавно Alexei Bezborodov вдохнул в проект новую жизнь, исправив несколько ошибок и, самое главное, выпустив новую версию.

Его ветка теперь является основной, а старая оставлена для истории.

Ниже анонс от Алексея.


Напишу здесь последние новости для интересующихся.

Появилась новая версия 0.1.0.

В ней добавлено:

  • Новый тип LuaScript::Double_LuaArg
  • Полноценный вектор LuaScript::Vector_LuaArg
  • Нестатические функции. Возможность передавать в функцию локальные объекты.
  • Новое описание.

Рекомендую посмотреть тесты. В них есть пример работы с двухмерным вектором и другие полезности.

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


Информация по теме:

пятница, 12 июня 2009 г.

Скрипты на Lua в С++

Lua отличный язык для встраеваемых сценариев. Продуманная структура языка, полный арсенал для процедурного и модульного программирования, а-ля функциональные возможности в виде функций-объектов и замыканий, работа со списками, кооперативная многопотоковость, зачатки объектно-ориентированности в достаточной мере для языка подобного рода, и вообще приятный синтаксис. Написан на стандартном С, поэтому отлично компилируется на разных платформах.

Есть великое множество оберток Lua для С++, но я не нашел ни одной, где не надо вообще вызывать С-шные функции Lua вручную из основной программы. Также для создания новых функций на С++, которые можно будет вызывать из Lua, должен быть только С++'ый подход.

Моя идея была в создании чисто плюсого интерфейса для Lua с максимально простой интеграцией в рабочий проект.

То, что пока вышло называется luascript.

Для включения в свой проект надо скопировать библиотеку в подкаталог luascript/ и добавить в проект два файла: luascript/luascript.cpp и luascript/lua/lua-files.c.

После этого можно писать вот такие куски кода:
lua script;
script.set_variable<lua::string_arg_t>("a", "test");
script.exec("b = a .. '123';");
std::cout << script.get_variable<lua::string_arg_t>("b").value());
Данный простой скрипт принимает строку через переменню a, добавляет к ней "123" и записывает результат в переменную b, которая потом подхватывается из С++.

Если надо добавить свою функцию, например, для проверки существования файла, можно написать так:
class file_exists_func_t {
public:
// Регистрируем аргументы функции. В данном случае один аргумент типа "строка".
static const lua::args_t* in_args() {
lua::args_t* args = new lua::args_t();
args->add(new lua::string_arg_t());
return args;
}

// Регистрируем выходные параметры. В данном случае это просто bool.
// Фукнция в Lua может возвращать не только одно значение, а несколько,
// поэтому можно задать список типов выходных параметров.
static const lua::args_t* out_args() {
lua::args_t* args = new lua::args_t();
args->add(new lua::bool_arg_t());
return args;
}

// Задаем namespace и, собственно, имя фукнции.
// Получается "fs.file_exits()".
static const std::string ns() { return "fs"; }
static const std::string name() { return "file_exists"; }

// Данный метод вычисляет значение функции.
// Сначала надо разобрать входные параметры, вычислить функцию и
// положить результы с массив выходных значений. Правильность
// работы с типами аргументов, выходных данных и индексов в массивах,
// их описывающих, лежит на плечах автора функции.
static void calc(const lua::args_t& in, lua::args_t& out) {
std::string filename = dynamic_cast<lua::string_arg_t&>(*in[0]).value();
std::ifstream is(filename.c_str());
dynamic_cast<lua::bool_arg_t&>(*out[0]).value() = is.good();
}
};
...
try {
// Создаем исполнителя скрипта.
lua script;
// Регистрируем нашу функцию "fs.file_exists()".
script.register_function< file_exists_func_t >();
// Устанавливаем переменную "fname" в "readme.txt".
script.set_variable<lua::string_arg_t>("fname", "readme.txt");
// Вызываем скрипт.
script.exec("exists = fs.file_exists(fname);");
// Получаем результат через переменную "exists".
bool exists = script.get_variable<lua::bool_arg_t>("exists").value();
} catch (lua::exception& e) {
std::cerr << "error: " << e.error() << ", line " << e.line();
}
Что пока не поддерживается, так это параметры типа таблица (хеш) для передачи их в функцию и получения их в качестве результата.

В каталоге lib лежат несколько мини примеров на Lua. Например, вот так можно вызвать внешнюю функцию для base64 кодирования или декодирования:
lua script;
script.exec("package.path = package.path .. ';./lib/?.lua'");
script.exec("require('base64'); a = base64.encode('test');");
// Данный пример напечатает "dGVzdA==".
std::cout << script.get_variable<lua::string_arg_t>("a").value();
Исходники доступны для просмотра в онлайне, или через Mercurial.

Сборка.

Пока я проверял только в Студии 2008. Тестовый проект включает в себя библиотеку, lua 5.1.4, Google Test 1.3.0 и несколько тестов, чтобы почувствовать вкус библиотеки. Все в одном флаконе.

Те, у кого есть SCons, могут собрать, набрав scons -Q. У кого нет, могут запустить скрипт compile-vs2008.cmd. Собранный runner для тестов luascript_unittest_vs2008.exe должен работать без ошибок. Посмотрев сами тесты в файле luascript_unittest.cpp можно в целом понять, как работать с библиотекой. Документация, конечно, будет, но пока так.

Общие замечания.

Забавно, в этих исходниках я попытался в качестве эксперимента максимально работать по стандарту кодирования Google. Из основного, что затронуло лично меня, это:
  • Отступ в 2 пробела (естественно, никаких табов). Для слов "public", "protected", "private" отступ в один пробел.
  • Максимальная экономия вертикального места (по возможности не лепить лишних пустых строк).
  • Открывающая скобка "{" практически всегда на той же строке (для классов, функций, циклов, условий и т.д.). Я раньше так не делал для классов и функций.
  • Никаких cast'ов в стиле С, даже для элементарных типов. Только приведения в стиле С++. Мне это очень нравится.
  • Забота о длинных строках. Как только можно избегать строк длинее 80 символов.
  • В именах закрытых членов класса использовать не "__" в качестве префикса, а "_" в качестве суффикса.
Это был снова эксперимент на Google Code и в opensource'e в целом. Если честно, то выкладывание исходников на публику страшно оздоравливает код, причем по всем статьям.

Данный проект не такой сухой как ранее выложенный SerialCom. Я с ним более менее активно работаю, так что должны быть точно улучшения. На работе, например, я его примастырил для гибко сконфирурированного фильтрования при журналировании. Есть, конечно, проблемы с производительностью (интерпретатор, все-таки, хоть и с виртуальной машиной), но есть пути улучшения.


Посты и ссылки по теме:

воскресенье, 29 марта 2009 г.

Триггер параллельных потоков для Windows и UNIX

Условные переменные, как и мьютексы, являются базовыми примитивами для синхронизации в параллельном программировании. К сожалению, классическая условная переменная в нотации потоков POSIX (pthread) сложно реализуема в Windows (судя по MSDN Windows таки поддерживают механизм условных переменных на уровне API, но не в XP или 2003, в чем-то более новом, увы). Мне потребовался для одного проекта простейший механизм синхронизации двух потоков: один поток ждет, не занимая ресурсов процессора, и активизируется, только когда другой поток его попросит об этом. Простейший триггер. Конечно, по логике — это обыкновенная условная переменная в упрощенном варианте. Для UNIX это реализуется именно через условную переменную потоков POSIX, а для Windows — через события.

Файл trigger.h:

#ifndef _EXT_TRIGGER_H
#define _EXT_TRIGGER_H

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

namespace ext {

class Trigger {
public:
Trigger();
~Trigger();

// Функция посылки сигнала потоку,
// ждущему на функции Wait().
void Signal();
// Функция ожидания сигнала.
// Вызов этой функции приводит к блокировке потока до
// получения сигнала от функции Signal().
// Внимание: функция Signal() не должна быть вызвана до
// того, как ждущий поток "сядет" на Wait(). Подобное
// использование ведет к неопределенному поведению.
void Wait();

private:
#ifdef WIN32
HANDLE __handle;
#else
pthread_mutex_t __mutex;
pthread_cond_t __cv;
#endif
// "Защита" от случайного копирования.
Trigger(const Trigger&);
void operator=(const Trigger&);
};

} // namespace ext

#endif
Файл trigger.cpp:
#include "Trigger.h"

namespace ext {

#ifdef WIN32

Trigger::Trigger() {
__handle = CreateEvent(
NULL, // Атрибуты безопасности по умолчанию.
TRUE, // Режим ручной активации события.
FALSE, // Начальное состояния -- неактивное.
NULL // Безымянное событие.
);
}

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

void Trigger::Signal() {
SetEvent(__handle);
}

void Trigger::Wait() {
// Ждем наступление события.
WaitForSingleObject(__handle, INFINITE);
// "Перезаряжаем" событие.
ResetEvent(__handle);
}

#else // WIN32

Trigger::Trigger() {
pthread_mutex_init(&__mutex, NULL);
pthread_cond_init(&__cv, NULL);
}

Trigger::~Trigger() {
pthread_cond_destroy(&__cv);
pthread_mutex_destroy(&__mutex);
}

void Trigger::Signal() {
pthread_mutex_lock(&__mutex);
pthread_cond_signal(&__cv);
pthread_mutex_unlock(&__mutex);
}

void Trigger::Wait() {
pthread_mutex_lock(&__mutex);
pthread_cond_wait(&__cv, &__mutex);
pthread_mutex_unlock(&__mutex);
}

#endif // WIN32

} // namespace ext
Пространство имен, как обычно, ext, так что меняете по вкусу.
Проверим, как будет работать (естественно, через тест).
Для тестирования также потребуются: класс Thread, класс PreciseTimer и Google Test. О том, как собрать себе компактную версию Google Test в виде всего двух файлов gtest-all.cc и gtest.h уже писал.
Файл trigger_unittest.cpp:
#include <gtest/gtest.h>

#include "trigger.h"
#include "thread.h"
#include "pretimer.h"

// Тестовый поток, который будет "скакать" по указанным ключевым
// точкам, увеличивая значение счетчика.
class TriggerThread: public ext::Thread {
public:
TriggerThread(volatile int& flag, ext::Trigger& trigger) :
__flag(flag), __trigger(trigger)
{}

virtual void Execute() {
// Ждем первого сигнала.
__trigger.Wait();
__flag = 1;
// Ждем второго сигнала.
__trigger.Wait();
__flag = 2;
// Ждем третьего сигнала.
__trigger.Wait();
__flag = 3;
}

private:
volatile int& __flag;
ext::Trigger& __trigger;
};

TEST(Trigger, Generic) {
volatile int flag = 0;
ext::Trigger trigger;

// Создаем поток и запускаем егою
TriggerThread a(flag, trigger);
a.Start();

// Подождем, чтобы поток "сел" на Wait().
ext::PreciseTimer::sleepMs(10);
// Флаг не должен стать 1, так как поток
// должен ждать на Wait().
EXPECT_EQ(0, (int)flag);

// Информируем поток о событии.
trigger.Signal();
// Подождем, чтобы поток успел изменить флаг на 1.
ext::PreciseTimer::sleepMs(10);
// Проверим, как он это сделал.
EXPECT_EQ(1, (int)flag);

// Далее проверка повторяется еще пару раз, чтобы проверить,
// что синхронизирующий объект правильно "взводится" после
// срабатывания.

trigger.Signal();
ext::PreciseTimer::sleepMs(10);
EXPECT_EQ(2, (int)flag);

trigger.Signal();
a.Join();
// Последняя проверка не требует ожидания, так как мы присоединись
// к потоку, и он точно уже завершился.
EXPECT_EQ(3, (int)flag);
}
Компилируем для Windows в Visual Studio:
cl /EHsc /I. /Fetrigger_unittest_vs2008.exe /DWIN32 runner.cpp ^
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest\gtest-all.cc
или в GCC:
g++ -I. -o trigger_unittest_vs2008.exe runner.cpp \
trigger.cpp trigger_unittest.cpp pretimer.cpp thread.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Trigger
[ RUN ] Trigger.Generic
[ OK ] Trigger.Generic (31 ms)
[----------] 1 test from Trigger (47 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (78 ms total)
[ PASSED ] 1 test.
Работает.
Внимательный читатель заметит, что по хорошему бы надо протестировать случай, когда функция Signal() вызывается раньше, чем слушающий поток дойдет до Wait(). Как сказано в комментариях, эта ситуация считается логической ошибкой и ведет к неопределенному поведению. В жизни получается так: реализация для Windows считает, что если функция Signal() была вызвана до Wait(), то Wait() просто тут же выходит, как бы получив сигнал сразу при старте. Реализация же под UNIX работает иначе: Wait() отрабатывает только те вызовы Signal(), которые были сделаны после начала самого Wait()'а. Самое настоящее неопределенное поведение. При использовании данного класса надо помнить об этом ограничении.

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

четверг, 5 марта 2009 г.

QueryPerformanceCounter() на мультиядерных и виртуальных системах

Как я обещал, рассказываю про мои приключения с классом PreciseTimer.

Мой класс PreciseTimer предназначен для работы с миллисекундными интервалами времени. Реализация под Windows основана на использовании функций QueryPerformanceFrequency() и QueryPerformanceCounter().

Этот класс активно используется в некоторых наших проектах. Также, в силу некоторых обстоятельств, мы активно используем виртуальные машины для тестовых сборок. И, например, сборка под Windows 64-бита производится под VirtualBox. И вот очередной релиз-кандидат ушел в тестирование. Немедленно мне посыпались жалобы, что сборка не работает под 64-битным Windows под виртуальной машиной.

Я запретил тестерам временно отключать тест и начал проверять все сам. На реальных машинах все работает. Начал гонять на виртуальных. На VMWare тоже глючит. Тест PreciseTimer.MeasurementAccuracy выдает ошибку типа:
c:\sandbox\test\PreTimer_unittest.cpp(22): error: Value of: delta <= allowed_delta_ms
Actual: false
Expected: true
Delta (100) > than 10
[ FAILED ] PreciseTimer.MeasurementAccuracy (110 ms)
Получается, что задержка в 100 миллисекунд была измерена практически как нулевая.

Я заподозрил функцию QueryPerformanceCounter(). Написал еще один кондовый тест:
TEST(PreciseTimer, MillisecCounter) {
monitor::PreciseTimer timer;
monitor::PreciseTimer::Counter a = timer.millisec();
timer.sleepMs(10000);
monitor::PreciseTimer::Counter b = timer.millisec();
EXPECT_EQ(10000, b - a);
}
Этот тест делает видную глазом задержку в 10 секунд (чтобы исключить проблему в самой задержке) и затем проверят показания таймера.

Итак, на реальной машине тест выдает следующее:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a

Actual: 9995
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter

1 FAILED TEST
Тест, конечно, сбоит, но тут четко видно, что требуемая задержка в 10000 миллисекунд (10 секунд) измерена как 9995 миллисекунд. Понятно, тут невозможно измерить точь в точь, но суть работает верно.

А вот, что я получил на виртуальное машине:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from PreciseTimer
[ RUN ] PreciseTimer.MillisecCounter
c:\sandbox\test\PreTimer_unittest.cpp(17): error: Value of: b - a
Actual: 90
Expected: 10000
[ FAILED ] PreciseTimer.MillisecCounter
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] PreciseTimer.MillisecCounter

1 FAILED TEST
Задержка в 10000 миллисекунд была измерена всего как 90. Вот и причина сбоя — функция QueryPerformanceCounter(). Полчаса работы.

Затем я поискал в интернете на тему проблем у функции QueryPerformanceCounter() на виртуальных машинах и нашел объяснение в MSDN. Корень проблемы, как оказалось, был не конкретно в виртуальных машинах, а в "старом" биосе и в использовании мультиядерных систем. На наших реальных мультиядерных машинах все работало, так как, видимо их биос был "нормальным".

В итоге проблема решилась добавлением параметра /usepmtimer в файл c:\boot.ini, как рекомендуется в найденной статье. После этого все тесты заработали как положено.

Я включил подробное описание проблемы в Release Notes, чтобы клиенты не наступили на эти грабли, и инцидент был исчерпан. Еще полчаса работы. Итого час на все.

А теперь вдумайтесь в произошедшее. Проблема была локализована и исправлена не то, чтобы до релиза. Она была локализована даже до тестового запуска. Лично я вот ну ни как не ожидал, что в Windows функция QueryPerformanceCounter() почему-то как-то особенно работает на мультиядерных системах со "старым" биосом (видимо биосы VMWare и VirtualBox как раз подходят под эту категорию). А вот как бы искал эту проблему потом? уже на реальной работающей системе. Одно из применений этого класса у нас, это измерения временных данных по транзакциям. Да я потратил был потом полжизни для поиска этой "маленькой проблемки", случись она у реального клиента.

Пишите тесты! Это экономит не только деньги, но самое драгоценное — ваши нервы.


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

четверг, 26 февраля 2009 г.

Unit-тест для Coredump

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

Модифицированный текст файла coredump.cpp, в котором с помощью макроса UNIT_TESTING встроена поддержка для тестирования. Если этот макрос определен, то, как я уже сказал, подавляется появление окна с ошибкой, и coredump файл создается с постоянным именем.

Файл coredump.cpp:
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

#ifdef UNIT_TESTING
lstrcpy(fname, "___coredump.dmp");
#else
// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);
#endif

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

#ifdef UNIT_TESTING
return EXCEPTION_EXECUTE_HANDLER;
#else
return EXCEPTION_CONTINUE_SEARCH;
#endif
}
Теперь, собственно, тест (файл coredump_unittest.cpp):
#include "gtest/gtest.h"

#include <fstream>
#include <windows.h>
#include <stdlib.h>

TEST(Coredump, CoredumpCreation) {
const char* coredump = "___coredump.dmp";

// На всякий случай заведомо стираем старые файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));

// Создаем файл с тестовой программой.
std::string program = "int main() { *(char *)0 = 0; return 0; }";
std::ofstream os("___coredump_main.cpp");
os << program << std::endl;
os.close();

// Компилируем тестовую программу с опцией UNIT_TESTING.
// С этой опцией coredump файл будет создаваться с постоянным
// именем "___coredump.dmp", и будет подавляется окно с сообщением
// об ошибке.
EXPECT_EQ(
0, std::system(
"cl /Zi /DUNIT_TESTING /Fe___coredump_main.exe"
" ___coredump_main.cpp coredump.cpp dbghelp.lib"
" 1>nul 2>&1"
)
);

// На всякий случая удаляем старый coredump файл.
std::remove(coredump);

// Убеждаемся, что файл действительно удалился.
std::ifstream isdel(coredump);
EXPECT_FALSE(isdel.good());
isdel.close();

// Запускаем тестовую программу.
ASSERT_EQ(0xC0000005, std::system("___coredump_main.exe"));

// Проверяем, создался ли файл coredump.dmp.
std::ifstream is(coredump);
EXPECT_TRUE(is.good());
is.close();

// Удаляем за собой временные файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));
std::remove(coredump);
}
Данный тест имеет ряд существенных недостатков. Во-первых, он использует файловую систему, и во-вторых, он вызывает компилятор, что занимает небольшое, но все же время. Недостатки неприятные, но в целом приемлемые.
Кстати, Google Test Framework умеет делать так называемые "смертельные" (death) тесты. То есть можно протестировать именно аварийное "падение" фрагмента кода, например, из-за нарушения защиты памяти, и для проведения такого теста не надо вручную компилировать что-либо, как мы делали тут. К сожалению, эта возможность основана на использования юниксового системного вызова fork() и поэтому доступна только на UNIX платформах.
Дежурный файл для запуска тестов runner.cpp:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Компилируем, например в Visual Studio 2008:
cl /EHsc /I. /Fecoredump_unittest_vs2008.exe /DWIN32 runner.cpp coredump_unittest.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Coredump
[ RUN ] Coredump.CoredumpCreation
[ OK ] Coredump.CoredumpCreation
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает.

Сразу скажу, я проверял все это только под Windows XP SP2 и Server 2003. Пожалуйста, сообщайте, если есть какие-то проблемы или тонкости под другими виндами.

Как это часто бывает в unit-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.


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

среда, 25 февраля 2009 г.

Coredump для Windows

Тривиальная задачка: что будет, если запустить вот такую программу?

Файл main.cpp:

int main() {
*(char *)0 = 0;
return 0;
}
В UNIX'е самым вероятным исходом будет сообщение:
Segmentation fault
Это означает безусловное падение из-за нарушения защиты памяти. Если пользователь разрешил создание coredump файлов (например, для командного интерпретатора bash это делается командой):
ulimit -c unlimited
то после запуска будет создан файл coredump. Этот файл фактически содержит образ памяти вашей программы в момент возникновения ошибки, и его можно открыть в отладчике, например в gdb:
gdb -c coredump
После чего командой bt (back trace) можно точно установить место в программе, в котором произошла ошибка. Естественно, для удобства, программа должна быть скомпилирована с включенной отладочной информацией.
Когда ошибка происходит в процессе отладки у вас на глазах, то проще запустить программу сразу под отладчиком. Но когда, например, ошибка происходит у заказчика, и вас нет рядом, то тогда можно попросить прислать вам coredump файл для анализа. Во многих случаях этого хватает для локализации проблемы.
А что делать под Windows? Запуск приведенной программы под виндами обычно приведет вот к такому сообщению:


Неискушенный пользователь обычно жмет "Don't send", и затем программа благополучно падает, и не останется никакой информации произошедшей ошибке. Конечно, программа может вести подробное журналирование, но часто ошибки подобного рода редко удается локализовать по журналам.

Я слышал, что может так случиться, что после нажатия "Send Error Report" Майкрософт с вами свяжется и поможет решить проблему. Со мной такого ни разу не случалось, увы.
В Windows тоже есть схожий механизм создания “на лету” образов памяти работающего процесса, но для его использования надо немного потрудиться.

Windows предоставляет механизм исключений, чем-то схожий с исключениями в С++ и системными сигналами в UNIX. На С++ эти исключения похожи try-catch синтаксисом, а на UNIX — тем, что можно перехватывать исключительные ситуации (например, ошибки) в программе типа нашей (для UNIX можно было бы перехватить сигнал SIGSEGV и получить управление при возникновении подобной ошибки).

Естественно, сигналы в UNIX используются не только для этого.
Итак, ниже я приведу исходный текст небольшого модуля, который будет создавать аналогичный по смыслу юниксовому coredump'у файл, по которому можно будет установить причину аварийного завершения программы. Все, что нужно — это прилинковать этот модуль в ваш проект. Ничего вызывать специально не надо. Модуль инициализируется автоматически.

Файл coredump.cpp:

#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

return EXCEPTION_CONTINUE_SEARCH;
}
Данный модуль создает статический объект coredumpInitializer, конструктор которого вызывается до функции main(). Конструктор устанавливает специальный обработчик для системных исключений, в котором в файл и записывается образ памяти текущего процесса при возникновении системных ошибок. Имя файла содержит идентификатор процесса, идентификатор потока и текущее время. Этот файл можно открыть в Visual Studio просто запустив его, либо в самой студии в меню File->Open->Project/Solution выбрать этот файл, указав его тип как "Dump files". Далее надо запустить программу через отладчик, нажав F5, и отладчик остановится на месте возникновения ошибки. Естественно, для работы на уровне исходного текста необходимо наличие файла с расширением .PDB, содержащего символьную информацию, в том каталоге, где расположен ваш coredump файл. Файл с раширением .PDB обычно создается при компиляции с включенной отладочной информацией, например, при использования ключа "/Zi".
Надо отметить, что если ошибка в вашей программе произойдет до функции main(), например, при инициализации каких-то статических объектов, то данный модуль может и не сработать, так как вызов конструктора объекта coredumpInitializer может быть запланирован уже после проблемного места. Вообще, статические переменные и объекты — это источник многих проблем, в основном из-за неопределенного порядка их инициализации, и их использования стоит избегать по возможности.
Итак, компилируем:
cl /Zi main.cpp coredump.cpp dbghelp.lib
Для компиляции необходима библиотека DbgHelp.lib. Она обычно входит в состав Windows SDK в составе студии. Также получившийся исполняемый файл main.exe будет использовать динамическую библиотеку DbgHelp.dll. DbgHelp.dll входит в состав так называемых redistributable файлов, то есть которые вы имеете право распространять вместе с программой на случай, если у клиента нет этой dll'ки. Эта dll'ка с большой вероятностью находится у вас в каталоге C:\WINDOWS\system32.
Теперь при запуске программы main.exe и после закрытия пользователем окна с сообщением об ошибке (см. картинку выше), будет создан .DMP файл с именем, начинающийся со слова coredump, например coredump-4584-3240-20090226022327093.dmp. Этот файл уже можно открыть в Visual Studio.

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

Пока я не придумал, как сделать для модуля coredump.cpp автоматизированный unit-тест. Проблема в том, что тут надо как-то подавить вывод окна с ошибкой (см. картинку выше). Если это сделать, то тест может быть вполне себе автоматизированным.
Более подробная информация вопросу создания образов памяти процесса есть в хорошей статье “Effective minidumps”.


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

вторник, 17 февраля 2009 г.

Автоматический мьютекс

Описанный мной ранее класс Mutex является базовым механизмом синхронизации потоков при параллельном программировании и применяется сплошь и рядом.

Часто бывают случаи, когда несколько функций реализуют какую-то единую функциональность, построенную на общем разделяемом ресурсе, защищенном блокировкой. В этом случае каждая функция в начале работы занимает эту блокировку, а на выходе — освобождает ее. Например, методы класса-регистратора системных событий все работают с выходным буфером и используют единую блокировку для синхронизации доступа к нему. Например:

class Logger {
public:
...
void put(const char* str) {
__lock.Lock();
__buffer.push_back(str);
__lock.Unlock();
}
void flush() {
__lock.Lock();
...
__buffer.clear();
__lock.Unlock();
}
...
private:
Mutex __lock;
std::vector<std::string> __buffer;
}
В целом, такой подход является не совсем правильным, так как данные методы могут быть весьма сложными, иметь многочисленные условные операторы, могут генерировать исключения. В этом случае программисту необходимо позаботиться о всех возможных вариантах завершения каждой функции и везде вставить оператор освобождения блокировки:
__lock.Unlock();
Если этого не сделать, то неосвобожденная по какой-то редко возникающей причине блокировка может просто "подвесить" всю программу, так как все остальные функции, работающие с этой блокировкой, более никогда не получат управления.

К счастью, в С++ есть механизм, дающий возможность очень просто избежать подобных проблем, вывозом кода освобождения блокировки при любом варианте завершения функции. Механизм называется RAII (Resource Acquisition Is Initialization). В С++ деструкторы созданных в контексте функции объектов обязательно вызываются перед завершением контекста (попросту говоря, когда функция завершается любым способом). Если возникло непойманное в функции исключение, то в процессе раскрутки стека деструкторы созданных локальных объектов тоже будут вызваны. Отсюда и идея: занимать блокировку в конструкторе созданного в функции локального объекта и затем освобождать ее в деструкторе. Использование такого метода позволило бы изменить приведенный пример так:

class Logger {
public:
...
void put(const char* str) {
AutoLock(__lock);
__buffer.push_back(str);
}
void flush() {
AutoLock(__lock);
...
__buffer.clear();
}
...
private:
Mutex __lock;
std::vector<std::string> __buffer;
}
Объект AutoLock, создаваемый первым в контексте каждой функции, будет занимать блокировку и освобождать ее при закрытии этого контекста.

Идея проста и понятна, а класс, реализующий эту логику еще проще.

Пространство имен ext можно заменить по вкусу на подходящее вам.
Файл autolock.h:
#ifndef _EXT_AUTOLOCK_H
#define _EXT_AUTOLOCK_H

#include "mutex.h"

namespace ext {

class AutoLock {
public:
// Запираем блокировку в конструторе
AutoLock(Mutex& lock) : __lock(lock) {
__lock.Lock();
}

// Освобождаем блокировку в деструкторе
~AutoLock() {
__lock.Unlock();
}
private:
// Защита от случайного копирования
AutoLock(const AutoLock&);
void operator=(const AutoLock&);

Mutex& __lock;
};

} // ext

#endif
Данный класс использует реализацию блокировки (мьютекса) Mutex.

Посмотрим, как оно будет в деле (конечно с помощью unit-тестирования).

Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл autolock_unittest.cpp:
#include "gtest/gtest.h"

#include "autolock.h"
#include "mutex.h"
#include "thread.h"

// Универсальная задержка в миллисекундах для Windows и UNIX
#ifdef WIN32
#include <windows.h>
#define msleep(x) Sleep(x)
#else
#include <unistd.h>
#define msleep(x) usleep((x)*1000)
#endif

// Тестовый поток
class T: public ext::Thread {
public:
// Параметры потока:
// flag - флаг для сигнализации о выполненном действии
// mutex - рабочий объект-блокировка
// timeout - время, которое необходимо подождать после
// установки флага
// val - значение, в которое надо установить флаг
T(volatile int& flag, ext::Mutex& mutex, int timeout, int val) :
__flag(flag), __mutex(mutex), __timeout(timeout), __val(val)
{}

// Функция потока: занять автоматическую блокировку, установить
// флаг, подождать указанное время, освободить автоматическую
// блокировку.
virtual void Execute() {
ext::AutoLock locker(__mutex);
__flag = __val;
msleep(__timeout);
}
private:
volatile int& __flag;
ext::Mutex& __mutex;
int __timeout;
int __val;
};

// Данный тест выполняет параллельно две функции, которые конкурируют
// за одну блокировку. Функция-поток 'a' занимает блокировку, устанавливает
// флаг в 1, ждет 100мс и затем освобождает блокировку. Функция-поток 'b'
// стартует, когда поток 'a' уже занял блокировку, поэтому после старта
// потока 'b' флаг еще некоторое время будет равен 1, пока поток 'a' не
// отпустит блокировку, и затем поток 'b' изменит флаг в 0, получив
// управление ожидания на блокировке.
TEST(AutoLock, ConcurrentCalls) {
volatile int flag = 0;

ext::Mutex mutex;

T a(flag, mutex, 100, 1);
T b(flag, mutex, 0, 0);

// Запускаем поток 'a'.
a.Start();
// Ждем, пока поток 'a' займет блокировку.
// Это случится, когда флаг станет равен 1.
while (!flag);

// Запускаем поток 'b'.
b.Start();
// Ждем немного, чтобы убедиться, что поток запустился
// и дошел до попытки занять блокировку.
msleep(50);

// Так как время задержки в потоке 'a' больше 50мс,
// то флаг все еще равен 1, так как поток 'a' пока не отпустил
// блокировку, не давая потоку 'b' получить управление
// и изменить флаг в 0.
EXPECT_EQ(1, flag);

// Ждем завершения потока 'a' (блокировка должна быть
// отпущена при его завершении.
a.Join();

// Ждем завершения потока 'b', который к своему завершению
// должен обнулить флаг.
b.Join();
EXPECT_EQ(0, flag);
}
Для компиляции нам также понадобятся файлы mutex.h (класс Mutex), thread.cpp и thread.h (класс Thread).

Файл для запуска тестов runner.cpp:

#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем:

Visual Studio:

cl /EHsc /I. /Feautolock_unittest_vs2008.exe /DWIN32 runner.cpp autolock_unittest.cpp thread.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o autolock_unittest_cygwin.exe runner.cpp autolock_unittest.cpp thread.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from AutoLock
[ RUN ] AutoLock.ConcurrentCalls
[ OK ] AutoLock.ConcurrentCalls
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает, что приятно. Тест работает как ожидалось. 
Не забудьте включить файл autolock_unittest.cpp в тестовый набор вашего проекта. Не тратьте время на вылавливание неожиданных глюков тогда, когда вы уже порядком подзабыли, как тут все работает. Пусть ловлей глюков занимается тест, автоматизировано.


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

суббота, 14 февраля 2009 г.

Шестнадцатеричная печать в STL поток

Когда-то очень давно я написал элементарный манипулятор для шестнадцатеричной печати в стандарный поток. Все просто и тривиально. Но тем не менее я заметил, что таскаю этот микрокласс почти в кажный проект, где нужна отладочная печать. Обычно для шестнадцатеричной печати надо указывать сразу несколько итераторов, типа:
std::cout << std::hex << std::uppercase << std::setfill('0') << std::setw(2) << 0xAA;
Причем std::setw() надо повторять для каждого нового выводимого элемента. Я свел все это в один итератор, чтобы можно было просто написать (указав итератору ширину выводимого поля):
std::cout << ext::Hex(2) << 0xAA;
Итак, класс Hex (название пространства имен можно подкрутить по вкусу), файл hex.h:
#ifndef _EXT_HEX_H
#define _EXT_HEX_H

#include <iostream>
#include <iomanip>

namespace ext {

class Hex {
public:
Hex(int width) : __width(width) {}
friend std::ostream& operator<< (std::ostream& os, const Hex& hex);
private:
int __width;
};

inline std::ostream& operator<< (std::ostream& os, const Hex& hex) {
std::hex(os);
std::uppercase(os);
os.width(hex.__width);
os.fill('0');
return os;
}

} // ext

#endif // _EXT_HEX_H
Теперь можно писать так:
std::cout << ext::Hex(0)  << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0x0a << std::endl;
std::cout << ext::Hex(1) << 0xaa << std::endl;
std::cout << ext::Hex(2) << 0xaa << std::endl;
std::cout << ext::Hex(4) << 0xaa << std::endl;
std::cout << ext::Hex(8) << 0x0a << std::endl;
std::cout << ext::Hex(16) << 0x0a << std::endl;
std::cout << ext::Hex(32) << 0x0a << std::endl;
И результатом будет:
A
A
AA
AA
00AA
0000000A
000000000000000A
0000000000000000000000000000000A
На всякий случай, unit-тест. Чтобы не было сюрпризов при обновлении компилятора, STLport или чего-то еще. Тест всегда проверит, работает ли класс так, как вы от него ждете. Вы можете возразить — ну класс-то выеденного яйца не стоит, а тут для него тесты... Соглашусь. А еще я соглашусь, что сотни раз самые казалось бы ненужные на первый взгляд тесты для "очевидных" классов помогали обнаружить глюки на новой версии системных библиотек, новой версии компилятора, использовании "более мощных" параметров оптимизации и т.д. Время на написание тестов всегда окупается сполна, всегда.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Файл hex_unittest.cpp:
#include "gtest/gtest.h"
#include "hex.h"
#include <sstream>

void testHex(int n, int w, const std::string& etalon) {
std::stringstream fmt;
fmt << ext::Hex(w) << n;
EXPECT_EQ(etalon, fmt.str());
}

TEST(HexManip, Generic) {
testHex(0x0A, 0, "A");
testHex(0x0A, 1, "A");
testHex(0xAA, 1, "AA");
testHex(0xAA, 2, "AA");
testHex(0xAA, 4, "00AA");
testHex(0xAA, 8, "000000AA");
testHex(0xAA, 16, "00000000000000AA");
testHex(0xAA, 32, "000000000000000000000000000000AA");
}
Ну и головная программа:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем.

Visual Studio:

cl /EHsc /I. /Fehex_unittest_vs2008.exe runner.cpp hex_unittest.cpp gtest\gtest-all.cc
Cygwin:
g++ -I. -o hex_unittest_cygwin.exe runner.cpp hex_unittest.cpp gtest/gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from HexManip
[ RUN ] HexManip.Generic
[ OK ] HexManip.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает как положено.
При использовании Hex у себя в проекте не забудьте включить файл hex_unittest.cpp в ваш набор unit-тестов. Оберегите себя от ненужной траты времени в будущем.
Под занавес пара слов о производительности. Очевидно, что если вы выводите в поток десятки тысяч шестнадцатеричных чисел подряд, то разумнее будет использовать стандартные итераторы — настроить поток с помощью std::hex, std::uppercase и std::setfill(), а потом вызывать только std::setw() для каждого нового элемента. Но если вы печатаете разнородные данные, что часто требуется при отладке, то тогда итератор Hex будет в самый раз.


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

четверг, 12 февраля 2009 г.

Универсальная информация об ошибках в Windows и UNIX

Достоверная информация об ошибках во время исполнения программы является залогом простой ее эксплуатации и поддержки. Вместо выслушивания от клиента стенаний на тему "я тут что-то нажал... а тут все не работает..." можно просто попросить его прислать файл журнала программы (log), и с большой вероятностью этого будет достаточно для локализации проблемы. С логическими ошибками бизнес логики программы все понятно — тут все зависит от вас, и вы точно знаете, какая ошибка произошла. Хуже обстоит дело с ошибками системными. Тут надо максималько точно опросить систему, что произошло, и по возможности, получить расшифровку когда ошибки.

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

Итак, привожу ниже класс, который я использую для получения информации об ошибке, произошедшей в операционной системе. Можно узнать код ошибки и его текстовое объяснение, если оно предоставляется. Это не бог весть какой сложный и оригинальный класс, но у меня он работает без каких-либо "допиливаний" на Windows 32- и 64-бит, Linux 2.6 32- 64-бит SPARC и Intel, Sun OS 5.10 SPARC и Intel, AIX, HP-UX и HP-UX IA64. К тому же, этот класс безопасен для мультипотокового использования (что лично для меня, например, очень важно).

Итак, класс SystemMessage. Все члены статические, так что можно работать с ними без создания экземпляра класса.
Пространство имен, как обычно, ext, так что измените, если необходимо.
Файл systemmessage.h:
#ifndef _EXT_SYSTEM_MESSAGE_H
#define _EXT_SYSTEM_MESSAGE_H

#include <string>

namespace ext {

class SystemMessage {
public:
// Эта функция возращает код ошибки.
static int code();
// Эта функция по коду ошибки возвращает ее текстовое описание, если
// таковое предоставляется операционной системой. Если нет, то
// возвращается строка "?".
static std::string message(int code);
};

} // namespace ext

#endif // _EXT_SYSTEM_MESSAGE_H
Файл systemmessage.cpp
#include "SystemMessage.h"

#ifdef WIN32
#include <windows.h>
#else
#include <string.h>
#include <unistd.h>
#include <errno.h>
#endif

namespace ext {

int SystemMessage::code() {
#ifdef WIN32
return GetLastError();
#else
return errno;
#endif
}

// Если система по какой-то причине не имеет функции strerror_r,
// то придется лазить напрямую в таблицу сообщений об ошибках.
// Для этого надо при компиляции определить макрос LIBC_NO_STRERROR_R.
// Пока я видел такое только на HP-UX IA64 v2.
#ifndef WIN32
#ifndef LIBC_NO_STRERROR_R
extern "C" int sys_nerr;
extern "C" char* sys_errlist[];
#endif
#endif

std::string SystemMessage::message(int code) {
char msg[1024];

#ifdef WIN32

// Версия для Windows
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
msg,
sizeof(msg) - 1,
NULL
);

char* p = msg + strlen(msg);

// Обрезаем c конца '\r', '\n' и '.'
for(p = msg + strlen(msg) - 1;
p >= msg && (*p == '\n' || *p == '\r' || *p == '.'); --p)
*p = 0;

#elif LIBC_NO_STRERROR_R

// Если UNIX-платформа не имеет функции strerror_r, то делаем ее
// работу вручную. Пока я встретил такое только на HP-UX IA64 v2.
if (code < 0 || code >= sys_nerr)
return "?";

strncpy(msg, sys_errlist[code], sizeof(msg) - 1);

// Если сообщение об ошибке длинее чем sizeof(msg)-1, то '\0'
// не будет скопирован, поэтому добавляем его вручну.
msg[sizeof(msg) - 1] = 0;

#else

// Для нормальной UNIX-системы просто вызываем strerror_r.
if (strerror_r(code, msg, sizeof(msg) - 1) < 0)
return "?";

#endif

// Поможем компилятору по возможности оптимизировать
// возвращаемое значение как rvalue.
return std::string(msg);
}

} // namespace ext
Теперь посмострим это в работе.
Я как-то пока не придумал, как универсально написать unit-тест для этого класса, так как предсказуемые результаты будут все равно различны для каждой платформы. А писать тесты под все платформы как-то топорно. Хочется гармонии, а тут пока ее нет. Если кто имеет идею, как универсально тестировать этот класс на всех платформах — поделитесь, пожалуйста.
Тестовая программа systemmessage_test.cpp:
#include <iostream>
#include <fstream>

#include "systemmessage.h"

int main(int argc, char* argv[]) {
// Пытаемся открыть заведомо несуществующий файл.
std::ifstream is("__non_existing_file__");

// Печатаем ошибку.
int error = ext::SystemMessage::code();
std::cout
<< error << ", "
<< ext::SystemMessage::message(error)
<< std::endl;

return 0;
}
Компилируем в Visual Studio:
cl /EHsc /Fesystemmessage_test_vs2008.exe /DWIN32 systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_vs2008.exe:
2, The system cannot find the file specified
Получили примерно ожидаемое виндовое сообщение об ошибке.

Теперь компилируем в Cygwin:
g++ -o systemmessage_test_cygwin.exe systemmessage_test.cpp systemmessage.cpp
Запускаем systemmessage_test_cygwin.exe:
2, No such file or directory
Получили сообщение об ошибке в стиле UNIX.
Повторюсь, в данном классе нет ничего удивительного и сложного. Просто это весьма универсальный и переносимый исходник.
И небольшая ремарка. В мире UNIX существует два диалекта функции strerror_r: XSI-версия (когда определен макрос _XOPEN_SOURCE, и он равен 600) и GNU-версия (доступная в libc, начиная с версии 2.0). Разница в том, что первая (XSI-версия) просто кладет сообщение об ошибке в предоставленный буфер и также возвращает код успешности или неуспешности своей работы в виде int'а. Нормальный UNIX-подход. Вторая версия (GNU) возвращает не int, а, собственно, указатель на строку с ошибкой, причем указываеть он может как на предоставленный функции буфер, так и куда-то еще, например, на какой-то внутренний буфер. Данный класс рассчитан на работу с XSI-версией функции strerror_r. Поэтому, если вдруг при компиляции этого класс на UNIX-системах вы получите сообщение об ошибке в использовании этой функции, то определите макрос _XOPEN_SOURCE в значение 600 (-D_XOPEN_SOURCE=600 для компилятора), тем самым будет принудительно использоваться XSI-версия этой болезной функции.

пятница, 6 февраля 2009 г.

Универсальный мьютекс на C++ для Windows и UNIX

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

Например, в программе есть некоторая строковая переменная, хранящая описание текущего состояния. Это состояние может, например, выводиться в нижней полоске рабочего окна. Теперь представим, что в программе есть два параллельно работающих потока. Первый занимается получением данных из сети, а второй — обработкой базы данных. Допустим, настал некоторый момент времени, когда первый поток принял данные из сети и хочет об этом отрапортовать в строке состояния, записав туда "Принято 16384 байт". Приблизительно в этот же момент второй поток завершил периодическую проверку базы данных и также желает об этом сообщить пользователю строкой "База данных проверена". Операция копирования строки не является атомарной, то есть нет никакой гарантии, что во время ее выполнения процессор не переключится на какой-то другой поток, и операция копирования не будет прервана посреди работы. Итак, когда поток номер 1 успел записать в строку состояния слово "Принято", может так случиться, что процессор активирует поток номер 2, который также начнет запись своей строки и добавит к уже записанному "Принято" строку "База данных про", но будет прерван первым потоком и т.д. В итоге в переменная может содержать кашу типа "ПрияноБаза данных 1про6в3ерена84 байт". Вывод такой — результат полностью непредсказуем.

Для решения подобного вроде проблем в мире параллельного программирования существует такое понятие, как блокировка. Суть ее в том, что когда один процесс захватывает блокировку, то все остальные процессы, пытающиеся ее захватить после, будут блокированы до тех пор, пока первый процесс ее не отпустит. Это похоже на дверь в комнату: представим, что наша переменная globalStatus находится в комнате с одной дверью и ключом внутри. Если дверь открыта (блокировка свободна), то в комнате никого нет (никто не работает с переменной). Когда процесс заходит в комнату, он запирает дверь изнутри (захватывает блокировку). После этого процесс может спокойно работать с переменной как угодно долго, так как гарантированно никто другой не войдет в комнату, так как она заперта изнутри, и не будет мешать ему работать с переменной.

Это была идея простейшей блокировки, которую часто называют мьютекс (mutex). Сейчас мы рассмотрим реализацию такой блокировки на С++, которая будет работать в Windows и UNIX. Как я писал в статье про параллельные потоки, в мире UNIX стандартом де-факто является библиотека pthread (POSIX Threads). Имеено ее мы и будем использовать для UNIX-версии. Для Windows будет отдельная реализация.

Класс Mutex получился весьма простой, в виде единственного файла mutex.h. Пространство имен (namespace) называется ext для простоты. Переименуйте его, если это требуется для вашего проекта.

Файл mutex.h:
#ifndef _EXT_MUTEX_H
#define _EXT_MUTEX_H

#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#define NOGDI
#include <windows.h>
#else
#include <stdlib.h>
#include <pthread.h>
#endif

namespace ext {

#ifdef WIN32
typedef CRITICAL_SECTION MutexType;
#else
typedef pthread_mutex_t MutexType;
#endif

// Интерфейс класса Mutex.
// Класс задумывался как маленький и быстрый, поэтому все
// определено прямо в заголовочном файле, и все функции
// объявлены принудительно inline. Это должно уберечь
// от ошибок и предупреждений о двойных символах при
// включении mutex.h в несколько модулей.
class Mutex {
public:
inline Mutex();
// Деструктор объявлен как не виртуальный из-за тех же
// соображений эффективности. Если вы планируете
// наследоваться от этого класса, то лучше сделать
// деструктор виртуальным, так как наследование от класса
// с не виртуальным деструктором потенциально опасно
// с точки зрения утечек памяти и является одним из
// больших "no-no" в С++.
inline ~Mutex();

// Функция захвата блокировки (вход в комнату и запирание
// двери ключом изнутри).
inline void Lock();

// Функция освобождения блокировки (отпирание двери и
// выход из комнаты)
inline void Unlock();
private:
MutexType __mutex;

// Защита от случайного копирования объекта данного класса.
// Экземпляр этого класса с трудом может быть нормально
// скопирован, так как он жестко привязан к системному
// ресурсу.
Mutex(const Mutex&);
void operator=(const Mutex&);
};

#ifdef WIN32

// Реализация через Windows API

Mutex::Mutex() { InitializeCriticalSection(&__mutex); }
Mutex::~Mutex() { DeleteCriticalSection(&__mutex); }
void Mutex::Lock() { EnterCriticalSection(&__mutex); }
void Mutex::Unlock() { LeaveCriticalSection(&__mutex); }

#else // WIN32

// UNIX версия через pthread

Mutex::Mutex() { pthread_mutex_init(&__mutex, NULL); }
Mutex::~Mutex() { pthread_mutex_destroy(&__mutex); }
void Mutex::Lock() { pthread_mutex_lock(&__mutex); }
void Mutex::Unlock() { pthread_mutex_unlock(&__mutex); }

#endif // WIN32

} // ext

#endif
Касаемо техники "защиты" объекта в С++ от случайного копирования я уже писал ранее.
Я не стал проверять коды возвратов данных функций для упрощения класса. Могу сказать, что если хоть одна из них завершиться с ошибкой, то это значит, что-то конкретно не так в вашей системе, и приложение по любому не будет работать нормально еще по миллиарду причин.
Пощупаем класс в работе. И конечно, используя unit-тестирование.
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Для теста нам также потребуются файлы thread.cpp и thread.h из статьи про параллельные потоки в С++.

Файл тестов mutex_unittest.cpp:
#include "gtest/gtest.h"

#include "mutex.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 A: public ext::Thread {
public:
// Передаем в конструкторе ссылку на флаг и
// ссылку на блокировку.
A(volatile int& flag, ext::Mutex& mutex) :
__flag(flag), __mutex(mutex)
{}

virtual void Execute() {
// Захват блокировки (1)
__mutex.Lock();
// Изменяет флаг на 1
__flag = 1;
// Освобождаем блокировку
__mutex.Unlock();
}

private:
volatile int& __flag;
ext::Mutex& __mutex;
};

TEST(MutexTest, Generic) {
// Начальное значение флага - 0.
volatile int flag = 0;

// Создаем объект-блокировку
ext::Mutex mutex;
// Захватываем блокировку.
mutex.Lock();

// Создаем параллельный поток выполнения.
A a(flag, mutex);
// Запускаем его.
a.Start();
// Ждем для проформы десятую секунды, чтобы дать
// время параллельному потоку создаться и успеть
// дойти до строки (1), то есть до захвата блокировки.
msleep(100);

// Значение флага должно быть все еще 0, так как
// параллельный поток должен быть блокирован на
// строке (1), так как мы захватили блокировку еще
// до его создания.
EXPECT_EQ(0, flag);

// Освобождаем блокировку, тем самым давая
// параллельному потоку выполняться дальше и
// изменить значение флага на 1.
mutex.Unlock();

// Ждем завершения параллельного потока.
a.Join();
// Так как параллельный поток завершился, то
// флаг теперь точно должен быть равен 1.
EXPECT_EQ(1, flag);
}
Для запуска тестов нам нужен стандартный файл запуска runner.cpp:
#include "gtest/gtest.h"

int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Компилируем все вместе.

В Visual Studio:
cl /EHsc /I. /Femutex_unittest_vs2008.exe /DWIN32 runner.cpp mutex_unittest.cpp thread.cpp gtest\gtest-all.cc
Или если вы используете gcc:
g++ -I. -o mutex_unittest_cygwin.exe runner.cpp mutex_unittest.cpp thread.cpp gtest/gtest-all.cc
Запускаем mutex_unittest_vs2008.exe или mutex_unittest_cygwin.exe:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MutexText
[ RUN ] MutexText.Generic
[ OK ] MutexText.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Вроде работает как надо.

Теперь внесем в исходный текст класса "случайную" ошибку, заменив строку:
void Mutex::Lock()         { EnterCriticalSection(&__mutex); }
на
void Mutex::Lock()         { /* EnterCriticalSection(&__mutex); */ }
Этой "ошибкой" мы просто отключили создание блокировки. Перекомпилируем все заново и запустим:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MutexText
[ RUN ] MutexText.Generic
mutex_unittest.cpp:41: Failure
Value of: flag
Actual: 1
Expected: 0
[ FAILED ] MutexText.Generic
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 0 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] MutexText.Generic

1 FAILED TEST
Видно, что флаг был изменен в параллельном потоке вне зависимости от блокировки (и понятно почему, мы ж ее "сломали").

Итак, можно вернуть исправленную строку в исходное состояние. Класс работает, и тесты на это подтвердили.
При использовании класса Mutex у себя в проекте не забудьте включить файл mutex_unittest.cpp в ваш набор 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.


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

четверг, 5 февраля 2009 г.

Обновление для миллисекундного таймера

Универсальный класс PreciseTimer, дающий возможность работать с миллисекундными интервалами времени в Windows и UNIX получил новую функцию:
static void sleepMs(int ms);
Эта функция реализует задержку в указанное число миллисекунд. Функция является статической, то есть ей можно пользоваться без создания экземпляра касса:
...
ext::PreciseTimer::sleepMs(100);
...
Необходимо учитывать, что в UNIX системах данная функция может быть прервана пришедшим системным сигналом, например, сигналом о полученных новых данных в буфер сокета. В этом случае задержка может быть меньне, чем ожидается.

Обновленный исходный текст класса и тестов находится по старому адресу.

четверг, 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-тестированию, и программируйте правильно!


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

среда, 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.


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