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

понедельник, 18 апреля 2011 г.

Потоки в C++ против потоков в Go

После поста про потоки в Go я прочитал другое мнение про общую целесообразность Go в плане продвинутости в многопоточном программировании.

Признаюсь, я не боец в бусте и новом C++, но благодаря предоставленному примеру, было очевидно, что и на С++ решение получается весьма изящное.

Интересно было сравнить производительнось потоков во обоих языках в плане скорости из создания и назначения им работы. Как я понял, это битва между pthreads и системой Go-рутин, которые не являются потоками операционной системы. Как сказано в документации:

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

Я взял последний boost, и на той же восьми процессорной машине провел эксперимент.

Программе надо будет выполнить множество однотипной работы (фактически, вызвать функцию). Задачи будут мультиплексироваться между несколькими параллельными потоками. Сама функция будет элементарной и быстрой. Надеюсь, этим удастся сфокусировать тестирование именно на подсистеме потоков, нежели на полезной нагрузке.

Итак, программа на Go:

package main

import (
        "flag"
        "fmt"
)

var jobs *int = flag.Int("jobs", 8, "number of concurrent jobs")
var n *int = flag.Int("tasks", 1000000, "number of tasks")

func main() {
        flag.Parse()

        fmt.Printf("- running %d concurrent job(s)\n", *jobs)
        fmt.Printf("- running %d tasks\n", *n)
        tasks := make(chan int, *jobs)
        done := make(chan bool)

        for i := 0; i < *jobs; i++ {
                go runner(tasks, done)
        }

        for i := 1; i <= *n; i++ {
                tasks <- i
        }

        for i := 0; i < *jobs; i++ {
                tasks <- 0
                <- done
        }
}

func runner(tasks chan int, done chan bool) {
        for {
                if arg := <- tasks; arg == 0 {
                        break
                }
                worker()
        }
        done <- true
}

func worker() int {
        return 0
}

Makefile для прогона по серии параметров:

target = go_threading

all: build

build:
        6g $(target).go
        6l -o $(target) $(target).6

run:
        (time -p ./$(target) -tasks=$(args) \
                1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }'

n = \
10000 \
100000 \
1000000 \
10000000 \
100000000

test:
        @for i in $(n); do \
                echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \
        done

Программа на C++:

#include <iostream>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <queue>
#include <string>
#include <sstream>

class thread_pool {

  typedef boost::function0<void> worker;

  boost::thread_group threads_;
  std::queue<worker> queue_;
  boost::mutex mutex_;
  boost::condition_variable cv_;
  bool done_;

 public:

  thread_pool() : done_(false) {
    for(int i = 0; i < boost::thread::hardware_concurrency(); ++i)
      threads_.create_thread(boost::bind(&thread_pool::run, this));
  }

  void join() {
    threads_.join_all();
  }

  void run() {
    while (true) {
      worker job;
      {
        boost::mutex::scoped_lock lock(mutex_);
        while (queue_.empty() && !done_)
          cv_.wait(lock);

        if (queue_.empty() && done_) return;

        job = queue_.front();
        queue_.pop();
      }
      execute(job);
    }
  }

  void execute(const worker& job) {
    job();
  }

  void add(const worker& job) {
    boost::mutex::scoped_lock lock(mutex_);
    queue_.push(job);
    cv_.notify_one();
  }

  void finish() {
    boost::mutex::scoped_lock lock(mutex_);
    done_ = true;
    cv_.notify_all();
  }
};

void task() {
  volatile int r = 0;
}

int main(int argc, char* argv[]) {
  thread_pool pool;
  int n = argc > 1 ? std::atoi(argv[1]) : 10000;

  int threads = boost::thread::hardware_concurrency();
  std::cout << "- executing " << threads << " concurrent job(s)" << std::endl;
  std::cout << "- running " << n << " tasks" << std::endl;
  for (int i = 0; i < n; ++i) {
    pool.add(task);
  }

  pool.finish();
  pool.join();

  return 0;
}

Makefile:

BOOST = ~/opt/boost-1.46.1

target = boost_threading

build:
        g++ -O2 -I $(BOOST) -o $(target) \
                -lpthread \
                -lboost_thread \
                 -L $(BOOST)/stage/lib \
                $(target).cpp

run:
        (time -p LD_LIBRARY_PATH=$(BOOST)/stage/lib ./$(target) $(args) \
                1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }'

n = \
10000 \
100000 \
1000000 \
10000000 \
100000000

test:
        @for i in $(n); do \
                echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \
        done

В обоих языках число потоков будет равно количеству процессоров - 8. Количество задач, прогоняемых через эти восемь поток будет варьироваться.

Запускаем программу на C++:

make && make -s test

g++ -O2 -I ~/opt/boost-1.46.1 -o boost_threading \
                -lpthread \
                -lboost_thread \
                 -L ~/opt/boost-1.46.1/stage/lib \
                boost_threading.cpp
(time -p LD_LIBRARY_PATH=~/opt/boost-1.46.1/stage/lib ./boost_threading  \
                1>/dev/null) 2>&1 | head -1 | awk '{ print $2 }'
     10000 0.03
    100000 0.35
   1000000 3.43
  10000000 29.57
 100000000 327.37

Теперь Go:

make && make -s test

6g go_threading.go
6l -o go_threading go_threading.6
     10000 0.00
    100000 0.03
   1000000 0.35
  10000000 3.72
 100000000 38.27

Разница очевидна.

Может быть я сравниваю соленое с красным, и результаты просто неадекватны. Будет очень признателен за подсказку, в каких попугаях на правильно измерять.

Посты по теме:

воскресенье, 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()'а. Самое настоящее неопределенное поведение. При использовании данного класса надо помнить об этом ограничении.

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

вторник, 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 в тестовый набор вашего проекта. Не тратьте время на вылавливание неожиданных глюков тогда, когда вы уже порядком подзабыли, как тут все работает. Пусть ловлей глюков занимается тест, автоматизировано.


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

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

Олег Цилюрик, Егор Горошко, "QNX/UNIX. Анатомия параллелизма"

Олег Цилюрик, Егор Горошко

QNX/UNIX. Анатомия параллелизма

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

Несмотря на присутствие слова QNX в названии книги, она является прекрасным руководством по программированию потоков в UNIX в целом. Начиная прямо от печки, то есть от определения потока и его порой неверного отделения от термина "процесс", далее, следуя по базовым способам синхронизации потоков (блокировки, семафоры, условные переменные), детально разжеваны все тонкости поточного программирования с точки зрения идей и концепций, и в конкретном применении их в потоках POSIX (pthreads), включая нестандартные расширения различных версий.

Отдельной строкой укажу на отличную главу про взаимодействие механизма сигналов UNIX'а и, собственно, потоков. Это весьма сложная тема, но тут она понятно расписана. Даны рекомендации “как надо” и “как не надо”. 

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

Даются сравнительные оценки работы потоков в различных UNIX'ах, например, QNX против Linux. Все плюсы и минусы обстоятельно, без эмоций разобраны. В конце книги рассматриваются некоторые чисто QNX'овые возможности: пулы потоков, и как QNX избавляет программиста от огромного количества головной боли при их использовании, методика программирования сервисов на основе сообщений (для QNX это вообще родная тема благодаря микроядру).

Авторы совершенно точно смогли сконцентрироваться именно на теме многопоточности, не тратя место в книге (всего ~280 страниц) на смежные вопросы, предоставив для “открытых” вопросов отличную библиографию.  

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

суббота, 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 будет в самый раз.


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

пятница, 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.


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

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


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

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


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