четвер, 28 липня 2011 р.

Використання бібліотеки Qt4 на прикладі простої OpenGL програми

У даній статті я розповім як використовувати OpenGL графіку у програмах основаних на бібліотеці Qt4 ну і власне про принципи роботи з самою бібліотекою. Я не буду ускладнювати задачу 3D графікою, обмежимося лише 2D. Створимо простеньку програмку з меню, одним пунктом у підменю та графічним віджетом. Намалюємо просту сцену з червоним квадратом на синьому фоні, квадрат можна буде рухати стрілками або мищкою та змінювати його розміри.

Для того щоб мати змогу використовувати Qt4 його потрібно спочатку встановити використовуючи менеджер пакетів. Можна також встановити чудову IDE KDevelop, але можна обійтися і найпростішим текстовим редактором.

Отже, перш за все нам потрібно створити віджет який буде відображати графіку. Робиться це не складно, у Qt взагалі дуже просто породжувати нові віджети, для цього потрібно просто створити новий клас як спадкоємець стандартного QGLWidget. Назвемо його наприклад MyGLWidget. По правилу прийнятому у Qt, кожен клас повинен описуватися своєю парою .h та .cpp файлів. Спочатку опишемо сам клас у заголовочному файлі myglwidget.h:

#ifndef MYGLWIDGET_H

#define MYGLWIDGET_H

#include

#include

class MyGLWidget : public QGLWidget

{

Q_OBJECT

public:

MyGLWidget(QWidget *parent = 0);

~MyGLWidget();

protected:

void resizeGL(int width, int height);

void paintGL();

void keyPressEvent (QKeyEvent *e);

void mousePressEvent(QMouseEvent *event);

private:

int iX, iY, iSize;

};

#endif

Тут особливу увагу потрібо звернути на використання макросу Q_OBJECT, він обробляється макропроцесором Qt і є обов'язковим для опису всіх класів віджетів. Інші методи перекривають стандартні. resizeGL - викликається при зміні розмірів віджету, paintGL - малює сцену, а keyPressEvent та mousePressEvent служать відповідно для перехвачення повідомлень від клавіатури та мишки відповідно. Самі методи реалізуються у файлі myglwidget.cpp (коментарі я додав по ходу програми):

#include "myglwidget.h"

MyGLWidget::MyGLWidget(QWidget *parent)

{

iX=125; iY=125; iSize=50; // задаємо початкові значення положення та розміру квадрату

}

void MyGLWidget::resizeGL(int width, int height)

{

if(height == 0) height = 1;

glViewport(0, 0, width, height);

glLoadIdentity();

if (width <= height) // Змінюємо систему координат таким чином щоб квадрат завжди залишався квадратом

glOrtho (0.0f, 250.0f, 0.0f, 250.0f*height/width, 1.0, -1.0);

else

glOrtho (0.0f, 250.0f*width/height, 0.0f, 250.0f, 1.0, -1.0);

}

void MyGLWidget::paintGL()

{

glClearColor(0.0f, 0.0f, 1.0f, 1.0f); // Задаємо синій колір для фону

glClear(GL_COLOR_BUFFER_BIT); // Очищуємо вікно

glColor3f(1.0f, 0.0f, 0.0f); // Обираємо червоний колір

glRectf(iX-iSize/2, iY+iSize/2, iX+iSize/2, iY-iSize/2); // Малюємо квадрат

glFlush(); // Виводимо зображення на екран

}

void MyGLWidget::keyPressEvent(QKeyEvent *e)

{

switch(e->key()) // Аналізуємо яку клавішу натиснено

{

case Qt::Key_Up: ++iY; break;

case Qt::Key_Down: --iY; break;

case Qt::Key_Left: --iX; break;

case Qt::Key_Right: ++iX; break;

case Qt::Key_Z: ++iSize; break;

case Qt::Key_X: --iSize; break;

default: QWidget::keyPressEvent(e); // Передаємо керування батківському методу

}

updateGL(); // Оновлюємо зображення

}

void MyGLWidget::mousePressEvent(QMouseEvent *event)

{

if (event->buttons() & Qt::LeftButton) { // Перевіряємо чи натиснуто ліву кнопку мишки

iX=event->x(); // Змінюємо положення квадрату

iY=height()-event->y(); // Зверніть увагу, у системах координат OpenGL та Qt вісь Y направлена у протилежні сторони

updateGL(); // Оновлюємо зображення

}

}

MyGLWidget::~MyGLWidget()

{ } // Деструктор залишаємо пустим

Все, ми створили новий графічний віджет. Тепер, нам потрібно створити клас основного вікна. Для цього потрібно створити спадкоємця для QMainWindow. Нехай новий клас буде називатися qt4gl, тоді файл qt4gl.h буде мати наступний вигляд:

#ifndef QT4GL_H

#define QT4GL_H

#include

#include

class QAction;

class QMenu;

class QTextEdit;

class MyGLWidget;

class qt4gl:public QMainWindow

{

Q_OBJECT

public:

qt4gl();

~qt4gl();

private:

void createActions();

void createMenus();

void readSettings();

MyGLWidget *glCanvas;

QMenu *fileMenu;

QAction *exitAct;

};

#endif

Ну і відповідно реалізація у файлі qt4gl.cpp:

#include

#include "qt4gl.h"

#include "myglwidget.h"

#include

qt4gl::qt4gl()

{

glCanvas = new MyGLWidget; // Створюємо екземпляр віджету MyGLWidget

setCentralWidget(glCanvas); // Робимо його центральним віджетом у вікні

createActions(); // Ініціалізуємо набір екшенів

createMenus(); // Створюємо меню

glCanvas->setFocus(); // передаємо активний фокус нашому графічному віджету

}

void qt4gl::createActions()

{

exitAct = new QAction(tr("E&xit"), this); // Створюємо екшен для реалізації виходу з програми

exitAct->setShortcut(tr("Esc")); // Задаємо коротку клавішу

exitAct->setStatusTip(tr("Exit the application")); // Задаємо підказку

connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); // Задаємо зв'язку сигнал-слот для виконання команди

}

void qt4gl::createMenus()

{

fileMenu = menuBar()->addMenu(tr("&File")); // Додаємо підменю у головне меню

fileMenu->addAction(exitAct); // Додаємо пункт меню вказавши відповідний екшен

}

qt4gl::~qt4gl()

{ }

Готово, основне вікно тепер створено. Для не знайомих з філософією Qt може бути незвичним наступний рядок:

connect(exitAct, SIGNAL(triggered()), this, SLOT(close()));

Сигналом у поняттях Qt називається подія яка виникає внаслідок тих або інших дій користувача, у даному випадку вибору пункта меню (triggered), але таких стандартних сигналів є досить багато. Слот - це метод який будучи зв'язаним з кокретним сигналом автоматично викликається при настанні тієї чи іншої події. Це основне з положень на якому базується Qt, у спеціальній літературі дане питання описано більш докладно.

Ну і на завершення створимо основний файл програми (main.cpp):

#include

#include "qt4gl.h"

int main(int argc, char *argv[])

{

QApplication app(argc, argv); // Ініціалізуємо програму

qt4gl * mw = new qt4gl(); // Створюємо основне вікно

mw->resize(250,250); // Задаємо розміри центрального віджета

mw->setWindowTitle(QObject::tr("OpenGL + Qt4")); // Вказуємо заголовок вікна

mw->show(); // Виводимо вікно на екран

return app.exec(); // Запускаємо головний цикл програми, return тут використовується для повернення результату виконання програми операційній системі, для *nix вимагається.

}

Ну от. Тепер залишається лише все це зкомпілювати. Для цього створюємо папку qt4gl, копіюємо туди всі наші файли і запускаємо команду:

qmake -project

У папці з програмою з'явиться файл проекта qt4gl.pro. Відкриємо його у будь-якому текстовому редакторі і додамо у його кінець наступний рядок (ця обція необхідна для роботи з OpenGL):

QT += opengl

Після збереження виконуємо команду:

qmake qt4gl.pro

Після завершення виконання даної програми препроцесор Qt створить Makefile під конкретну платформу. Тепер вже можна запускати безпосередньо компіляцію програми:

make

Якщо код набрано без помилок, то по завершенню компіляції у цій же папці з'явиться файл qt4gl. Це і є готовий бінарний файл з програмою. Запустимо його:

./qt4gl

Результатом буде наступне:

Квадрат можна рухати стрілками на клавіатурі або просто клацнувши у будь-якому місці мишкою, клавішами Z та X можна змінювати розмір квадрату. Щоб закрити вікно можна обрати у меню File пункт Exit, або просто натиснути Esc. Зміна розмірів вікна призведе до автоматичного масштабування і квадрату. Тобто все працює так як і було задумано.

На завершення, хочу порекомендувати книжку C++ GUI Programming with Qt 4 By Jasmin Blanchette, Mark Summerfield. Дуже легко читається і у той же час все докладно пояснюється.

вівторок, 26 липня 2011 р.

Система CMake - частина 1

Ось мій варіант тесту для справжнього програміста: що найнеприємніше не знайти? Кредитку, корою щойно розплачувався в ресторані, телефон прекрасної дівчини, з якою вчора познайомився в метро або файл збірки проекту для тієї системи, в якій працюєш? Зрозуміло, жодна з цих проблем не варта того, щоб робити з неї трагедію. Навіть якщо хтось зуміє скористатися вашою кредиткою, гроші, швидше за все, вдасться повернути, над втратою випадкового телефонного номера теж горювати варто - хто знає, може воно й на краще, а відсутній файл збірки проекту можна відновити, за умови, що ви розумієте структуру проекту і у вас під рукою є відповідні інструменти.

Програмісти, що працюють в Linux (та й в інших Unix-системах) традиційно використовують для збірки програм інструментарій GNU build system (який складається з утиліт autoconf , automake, libtool, gnulib). Саме за допомогою GNU build system створюються знайомі всім нам файли configure, за допомогою яких ми генеруємо make-файли для збірки додатку. Система GNU build system настільки тісно пов'язана з історією Linux і інших відкритих проектів, що «мантра» configure - make - make install розглядається деякими Unix-програмістами як єдино вірний спосіб установки ПЗ. До недоліків GNU build system можна віднести її орієнтацію на інструментарій розробника GNU (GNU make, GCC і т.п.), який дуже популярний на відкритих Unix-платформах, але не популярний в інших середовищах. Прагнучи заповнити цю прогалину, розробники з компанії Kitware створили свій власний варіант крос-платформового генератора складальних файлів - CMake - (cross-platform make).

Система CMake обрана в якості стандартного засобу збирання KDE 4, і вже по одній цій причині заслуговує нашої уваги. Але у CMake є і власні переваги. Перш за все, CMake є по-справжньому крос-платформовим генератором проектів, що дозволяє створювати єдині описи проектів для Windows, Linux та інших Unix-систем (включаючи Mac OS X). Зупинимося на цьому детальніше. Важлива відмінність Windows від Linux (з точки зору розробника) полягає в тому, що на платформі Windows немає єдиного стандарту складальних файлів. MS Visual Studio використовує свої файли проектів, C++ Builder - свої, MinGW - свої. Перевага CMake в тому, що ця система здатна генерувати «рідні» складальні файли для всіх перерахованих засобів розробки (як і для багатьох інших). Крім того, CMake прагне максимально використовувати фірмові засоби генерації складальних файлів - наприклад, для генерації складальних файлів проекту Qt використовується qmake. Крім іншого CMake володіє інтелектуальною системою пошуку інструментів побудови та бібліотек на конкретній платформі (інтроспекція) і автоматичного конфігурування. Завдяки цьому система CMake сама встановлює багато параметрів складальних файлів, які в інших системах управління збіркою доводиться встановлювати вручну. Наприклад, в ОС Linux CMake сам знайде директорію, в якій ви встановили потрібний програмі набір віджетів, а під Windows вам не доведеться вказувати CMake, де встановлені Visual Studio або C++ Builder. Все це робить пакет CMake дуже простим і зручним у використанні.

Хоча в цих статтях ми будемо розглядати роботу CMake в основному на прикладі програм, написаних на C++, CMake може стати в нагоді і тим, хто пише на Java (а також на багатьох інших мовах).

Схематично роботу CMake можна описати таким чином: для складання програми створюється файл CMakeLists.txt, в якому описуються параметри збірки (розташування файлів вихідних текстів, необхідні зовнішні модулі, цілі збірки). Далі цей файл передається утиліті cmake. Результатом роботи cmake є файл, що містить інструкції складання програми для конкретної платформи (make-файл GNU make, файл проекту Visual Studio і т.д.). Суть ідеї полягає в тому, що опис процесу складання у файлі CMakeLists.txt абстраговано як від конкретних особливостей окремих систем (розташування файлів, можливості компіляторів), так і цілих платформ. Читаючи загальний опис процесу складання з файлу CMakeLists.txt, програма cmake створює файл інструкцій збірки, що враховує специфіку конкретної системи./*далі не відредаговано*/

Як же програмі CMake вдається бути такою «розумною»? В основі CMake лежить потужна мова сценаріїв, який використовується як в файлах CMakeLists.txt, так і в спеціальних сценаріях - модулях. Саме в модулях реалізовані такі функції CMake, як пошук файлів і перевірка можливостей системи. Файли модулів розташовані в піддиректорії Modules директорії cmake. Файли модулів можна розділити на три категорії. Одні файли призначено для налаштування роботи системи на конкретній платформі (Linux, Winodws, Mac OS, BeOS і т.д.). Друга група модулів призначена для виконання інтелектуального пошуку засобів розробки і різних допоміжних утиліт, а також для перевірки можливостей цих коштів. Третя група модулів виконує пошук і перевіряє параметри різних бібліотек, необхідних окремим програмам. Важливу роль у роботі CMake грають так само файли шаблонів для генерації різних файлів збірки. Оскільки файли модулів і шаблонів відокремлені від самої утиліти cmake і написані тією ж мовою сценаріїв, що і файли опису збірки, розширити можливості CMake дуже просто. CMake 2.6 поставляється з трьома сотнями модулів, які адаптують CMake для роботи з безліччю різних інструментів, починаючи з компіляторів C++ та Fortran, інтерпретаторів Perl і Python і закінчуючи такими бібліотеками як OpenSceneGraph, SDL, Qt 4 і wxWidgets . Файли модулів поставляються не тільки з дистрибутивами CMake, а й разом з дистрибутивами деяких бібліотек, розробники яких вже зацікавлені в підтримці CMake.

Хоча основні переваги CMake пов'язані з крос-платформенностью, що і відображено в назві пакету, простота роботи з різними бібліотеками може сдлеать CMale корисним і для Linux-проектів, які не передбачають перенесення на інші платформи. Завдяки CMake ви отримуєте простий і стандартний спосіб складання додатків, що використовують Qt, KDE, GTK +, wxWidgets ...

Приступаючи до роботи

Всі приклади, розглянуті в цих статтях, орієнтовані на CMake 2.6 або більш пізніх версій. Якщо у вас встановлена ​​більш рання версія CMake, поновіть її - це нескладно (скачати останню версію CMake можна з сайту www.cmake.org). Хоча виклад у цих статтях орієнтоване, в основному, на Linux-програміста, ми не будемо нехтувати крос-платформеними можливостями CMake, так що якщо ви все ще користуєтеся ОС з Редмонда, рекомендується встановити CMake і на неї теж. Для вивчення крос-платформних можливостей CMake ми скористаємося крос-платформних набором віджетів wxWidgets (останню версію оного я теж рекомендую встановити на всі ваші платформи з сайту wxwidgets.org). Якщо ви ніколи не програмували в wzxWidgets і не мають наміру робити цього надалі, то я можу вас заспокоїти - ми не будемо займатися програмуванням wxWidgets. Ми зараз вивчаємо систему збірки програм, а не їх написання, тому в якості навчального матеріалу нам цілком підійдуть численні приклади готових програм, що входять до складу дистрибутива wxWidgets. Оскільки ми практично не будемо торкатися внутрішньої начинки цих програм, майже всі наші рекомендації та інструкції можна розглядати як універсальні, що застосовуються при роботі з будь-якою іншою бібліотекою віджетів. Нашим основним засобом розробки буде, природно GCC, а в огляді перенесення проектів під Windows ми розглянемо (дуже побіжно) збірку проектів за допомогою Visual Studio і C + + Builder.

Перший приклад

У директорії samples дистрибутива wxWidgets знайдіть піддиректорію minimal. Ця піддиректорії містить мінімальний приклад програми wxWidgets. У піддиректорії minimal містяться також файли збірки додатку на всі випадки життя, а от файлу CMakeLists.txt в ній немає. Ми виправимо це упущення. Нижче наводиться вихідний текст файлу CMakeLists.txt (який ви знайдете на диску) для програми minimal. Для спрощення роботи файл CMakeLists.txt розміщується в тій же директорії, що і вихідні тексти програми.

project (minimal) cmake_minimum_required (VERSION 2.6) set (wxWidgets_USE_LIBS base core) find_package (wxWidgets REQUIRED) include ($ {wxWidgets_USE_FILE}) set (minimal_SRCS minimal.cpp) if (WIN32) set (minimal_SRCS $ {minimal_SRCS} minimal.rc) endif (WIN32) add_executable (minimal WIN32 $ {minimal_SRCS}) target_link_libraries (minimal $ {wxWidgets_LIBRARIES})

Ті, кому відразу не терпиться спробувати, можуть скомандувати в директорії minimal

cmake. /

після чого викликати make. Процес складання за допомогою make-файлу, згенерованого CMake, виглядає досить барвисто (рис. 1).

Малюнок 1. Make-файли CMake в консолі Linux.

Після закінчення збірки можна скомандувати

minimal

і подивитися на роботу програми, яку ми не написали, але зібрали (рис. 2).

Рисунок 2. Додаток wxWidgets під Linux.

Перш ніж розбирати цей приклад, розглянемо коротко структуру мови сценаріїв CMake. Протягом усіх наступних статей ми, в основному будемо займатися вивченням цієї мови. Елементи мови сценаріїв CMake можна розділити на три категорії: змінні, властивості і команди. Змінні грають у проектах CMake таку ж важливу роль, як і у файлах make. Налаштування параметрів збірки проекту виконується, в основному, шляхом присвоєння змінним різних значень. За допомогою властивостей проект CMake отримує різні відомості про стан системи, для якої генеруються складальні файли. Властивості можна розглядати як змінні, доступні лише для читання. Значення властивостей встановлюються середовищем CMake. Команди, як ви, звичайно, здогадалися, виконують різні дії із змінними і властивостями CMake і керують процесом генерації файлів збірки. Важливою особливістю мови CMake є його розширюваність. Модулі CMake, про які говорилося вище, дозволяють визначати нові змінні, властивості і команди. Саме завдяки розширюваності мови CMake цей пакет може використовуватися для складання таких різних проектів.

Де знайти документацію

Загальний опис синтаксису мови CMake можна знайти в інтернеті на сторінці cmake.org / HTML / Documentation.html . Там же є посилання на паперову книгу Bill Hoffman, Mastering CMake (наскільки я знаю, російською мовою не видавалася). Що стосується розширень CMake, описи багатьох з них входять в стандартну документацію. Опис інших розширень слід шукати на сайтах тих проектів, для яких вони призначені.

Не слід думати, що розширення мови CMake надмірно його ускладнюють. Базовий набір команд змінних і властивостей мови не дуже великий, а розширення, як правило, пов'язані з конкретними пакетами, середовищами та платформами розробки. Якщо ви не ставите перед собою мету стати ерудитом в області CMake, вам потрібно буде знати тільки ті розширення, які потрібні для використовуваних вами інструментів, а їх, як правило, теж не багато.

Перейдемо тепер до розгляду файлу CMakeLists.txt. Опис збірки починається з команди project ()

Команда project () задає ім'я проекту, а також дозволяє вказати (як необов'язкового параметра) мова програмування. Як ім'я проекту ми вибираємо ім'я модульна програми (це не обов'язкове правило CMake, але так зручніше). Мова програмування (С, CXX, Java) можна не вказувати. У цьому випадку CMake спробує «вгадати» мову по розширень файлів вихідних текстів. Команда project () - не просто елемент оформлення файлу CMakeLists.txt. Крім іншого, ця команда визначає змінні projectname_SOURCE_DIR і projectname_BINARY_DIR. Як неважко здогадатися, ці змінні містять імена директорії вихідних тестів (звичайно та ж директорія, в якій розташований файл CMakeLists.txt) і директорії в якій буде збережений результат збірки. Файли програм за замовчуванням збираються в тій же директорії, де розташований файл CMakeLists.txt, а файли бібліотек - в піддиректорії lib цієї директорії.

З великої літери чи з маленької?

Мова CMake має часткової регістрозалежному незалежністю. Імена команд не залежать від регістру (IF (), If (), if () і iF () - одна й та ж команда), у той час як імена властивостей і змінних - залежать. У зв'язку з регістру незалежністю імен команд виникає запитання, як правильніше писати - великої літери, з маленькою або повністю заголовними буквами. Різні автори пишуть по-різному (в документації CMake імена команд пишуться повністю малими буквами, а в багатьох файлах CMake, написаних самими розробниками - великими). Особисто я пишу імена команд з маленької літери, і буду дотримуватись цього принципу в статтях цієї серії.

Команда cmake_minimum_required () дозволяє встановити мінімальний номер версії CMake, необхідний для складання проекту. Ця команда не є обов'язковою, але вона дуже корисна, якщо файл CMakeLists.txt використовує можливості, що з'явилися в останній версії CMake, яка поки що встановлена ​​не у всіх. Якщо встановлена ​​в системі версія CMake не відповідає мінімальним вимогам файлу CMakeLists.txt, буде видано повідомлення про помилку.

Наступна команда, set (), є однією з найбільш часто використовуваних команд в файлах CMake. Команда set () дозволяє присвоювати значення змінним. Першим аргументом команди повинно бути ім'я змінної (якщо змінної з цим ім'ям раніше не існувало, вона буде створена), далі йде список значень, що присвоюються змінної (однієї змінної можна привласнити відразу кілька значень). Наприклад, в команді

set (wxWidgets_USE_LIBS base core)

Ми присвоюємо змінної wxWidgets_USE_LIBS (яка повинна містити список необхідних програмі модулів wxWidgets) значення base і core. Змінні CMake можна використовувати рекурсивно, тобто додавати нові значення до списку вже наявних, як наприклад, у рядку

set (minimal_SRCS $ {minimal_SRCS} minimal.rc)

де до значень змінної minimal_SRCS додається ще одне значення. Зверніть увагу на конструкцію $ {ім'я_змінної}. Отак конструкція дозволяє отримати значення змінної з вказаним ім'ям (без символу $ і фігурних дужок CMake «не зрозумів» б, що тут мається на увазі ім'я змінної). Правила використання імен і значень змінних в CMake схожі на правила для змінних оточення в сценаріях оболонки.

Команда find_package () - одна з найважливіших команд CMake. У наступній статті ми зупинимося на цій команді докладніше, зараз скажемо тільки, що find_package () завантажує розширення CMake. У нашому прикладі завантажується розширення wxWidgets, призначене для генерації файлів збірки програм, що використовують wxWidgets. Важливо підкреслити, що команда find_package () не шукає самі бібліотеки, вона тільки завантажує розширення CMake. Пошук бібліотек, визначення їх версій та інші дії, пов'язані з бібліотеками, виконує завантажене розширення. Природно, що оскільки система CMake сама не виконує складання додатків, бібліотеками вона «цікавиться» лише з точки зору генерації інструкцій для складальних файлів. У нашому прикладі команда find_package () викликається з опцією REQUIRED. тим самим ми вказуємо CMake, що якщо розширення для роботи з wxWidgets не знайдено, генерація файлів, необхідних для складання програми, виконуватися не може.

Команда add_executable () відноситься до числа команд, які встановлюють цілі збірки. Дана команда вказує, що метою складання є здійснимий файл програми. Якби ми збирали бібліотеку, слід було б використовувати команду add_library (). Команда add_executable () дозволяє вказати ім'я зібраного файлу і пов'язати з ним вихідні тексти. Зверніть увагу на опцію WIN32, передану команді add_executable (). Цю опцію потрібно згадати для того, щоб при генерації файлів збірки на платформі Windows були створені файли збірки графічного програми (а не консольного, як це буде за замовчуванням). В Linux немає принципової різниці між структурою исполнимого файлу графічної і консольної програми (в Widows - є, цікавляться читачів відсилаю до відповідної літератури). Як же відреагує CMake для Linux на присутність опції WIN32? правильна відповідь - ніяк, пакет її просто проігнорує. Крім файлів програм і бібліотек у проектах CMake можна вказувати і інші цілі, наприклад, встановлення та упаковку додатків.

Остання команда файлу target_link_libraries () вказує CMake, які інструкції слід додати в складальний файл для підключення до проекту сторонніх бібліотек. Перший аргумент команди - мета, визначена вище командою add_executable () (взагалі кажучи, цілей складання може бути декілька), далі йде список зовнішніх бібліотек, яких потребує ця мета. У нашому прикладі цей список зберігається у змінній wxWidgets_LIBRARIES, визначеної в розширенні wxWidgets. На вміст змінної wxWidgets_LIBRARIES впливає мінлива wxWidgets_USE_LIBS, ось чому значення цієї змінної слід присвоїти до звернення до команди find_package ().

Збірка c допомогою CMake на платформі Windows

Наш файл CMakeLists.txt вже містить деякі елементи, які повинні стати в нагоді при генерації складальних файлів під Windows. Проте, на цій платформі нас чекають деякі складності. Викликані ці складності тим, що файлова система Windows набагато більш хаотична, ніж файлова система Linux. В Unix-системах існують стійкі традиції щодо того, де повинні бути розташовані ті чи інші файли. Наприклад, файли поділюваних бібліотек слід шукати (і встановлювати) в директоріях / lib, / usr / lib, / usr / local / lib. У Windows плутанини набагато більше. Файли тих же поділюваних бібліотек можуть розташовуватися або в системних каталогах Windows, або у власних каталогах програм. На додаток до цього файлова система Windows не має спільного кореня, як це прийнято в Unix-систм. Всі ці особливості призводять до того, що під Windows пакету CMake набагато важче знайти місця розташування необхідних бібліотек. Беручи до уваги цей факт, а також те, що консоль в Windows набагато менш популярна, в середовищі Windows рекомендується використовувати спеціальну утиліту з графічним інтерфейсом (рис. 3). Така ж програма є і в пакеті CMake для Linux, але в нашій улюбленій ОС зручніше користуватися консоллю.

Малюнок 3. CMake GUI для Windows.

Головне завдання CMake GUI - генерація файлів збірки додатку на основі файлів CMakeLists.txt для різних засобів збирання, використовуваних на платформі Windows. Попутно ця утиліта дозволяє присвоїти значення деяким змінним CMake, для яких сам пакет CMake не зміг знайти відповідні значення (або «не впевнений» в їх правильності). Зазначимо, що ці додаткові настройки впливають на одержувані в результаті генерації файли збірки, але не впливають на вміст самого файлу CMakeLists.txt.

Генерація файлів збірки з допомогою CMake GUI складається з двох етапів: настройки і власне генерації. Перша операція виконується за командою Configure, друга - за командою Generate. У процесі конфігурації утиліта дозволяє вам вибрати середовище розробки з досить значного списку і налаштувати значення «сумнівних» змінних. В процесі генерації утиліта може виявити синтаксичні помилки у файлі CMakeLists.txt. Якщо в якості цільової середовища збірки обрано середовище Visual Studio, CMake для Windows генерує файл ALL_BUILD.vcproj. Якщо метою є компілятор Borland C + +, в результаті створюється make-файл для консольної версії компілятора (рис. 4).

Малюнок 4. Консольна збірка під Windows виглядає майже так само, як і під Linux. Ось вона, крос-платформенность! ;)

Виконаємо збірку, ми отримуємо той же додаток, що і під Linux (рис. 5)

Малюнок 5. Програма minimal, Windows-версія.

У цій статті ми лише побіжно ознайомилися з мовою CMake. У следующнй статті нас чекає більш детальне знайомство зі змінними і командами мови CMake, а так само з ключами, що дозволяють управляти консольним варіантом програми.

Приклад проекту CMake для цієї статті

Розширення Boost.Asio

/*стаття не відредагована*/
Я вже досить давно використовую бібліотеку Boost.Asio у своєму проекті, в основному для організації асинхронного введення виведення і не раз ловив себе на думці, що хотів би використовувати її не тільки для цього. Насправді це не складно виправити, asio це не стільки мережева бібліотека, скільки дуже зручний фреймверк для вашого коду =) Отже, трохи теорії. Бібліотека asio дозволяє ефективно відокремити реалізацію від інтерфейсу використовуючи принцип dependency injection, для цього існують три сутності: io_object - інтерфейс надається користувачеві; service (не плутати з io_service-ом) - об'єкт реалізує взаємодію з ОС, наприклад з системою введення-виведення; implementation - зберігає стан об'єкта (наприклад хендл сокета). Все разом це виглядає так: io_object - клас успадкований від boost:: asio:: basic_io_object, Об'єкти цього класу створюються користувачем для виконання будь-яких операцій, приклади: boost:: asio:: deadline_timer; boost:: asio:: ip:: tcp:: socket; boost:: asio:: windows:: random_access_handle. Конструктор класу basic_io_object приймає покажчик на io_service. Кожному типу io_object-а відповідає свій service. Сервіс, це об'єкт безпосередньо виконує операції, програма користувача повинна взаємодіяти з сервісом не безпосередньо а через io_object відповідного типу, який виконує роль інтерфейсу. Сервіс автоматично створюється до створення першого примірника відповідного io_object-a, наприклад, для всіх сокетів створюється один сервіс (під windows це буде win_iocp_socket_service за назвою зрозуміло, що він використовує порти завершення:), для таймерів - інший (deadline_timer_service, котрий під windows так- ж використовує IOCP). io_service містить список сервісів для всіх об'єктів (io_object) які були на ньому створені (при створенні отримали посилання на цей io_service як параметр конструктора). basic_io_objectмістить два члени класу, з допомогою яких програміст може отримати доступ до сервісу та реалізації самого об'єкта - service і implementation. service - посилання на сервіс для даного об'єкта, який має тип basic_io_object:: Service_type, implementation - об'єкт типу basic_io_object:: Implementation_type - який є членом класу basic_io_object і представляє з себе реалізацію io_object-a, часом життя цього об'єкта управляє сервіс (волаючи методи construct і destroy). Допустимо у нас є такий код: boost:: asio:: io_service io; boost:: asio:: deadline_timer timer (io); У першому рядку буде створено io_service, він не міститиме жодного сервісу і якщо ми викличемо метод io.run (), він поверне управління відразу, тому що роботи в нього немає. У другому рядку все трохи цікавіше, спочатку буде створений io_object, в даному випадку це deadline_timer, в конструкторі basic_io_object-a (базового класу) буде викликана ф-я use_service яка спробує знайти відповідний сервіс (для таймерів він буде мати тип deadline_timer_service <... >) якщо для даного io_service-a такий сервіс ще не створено, то він бует створений, далі буде викликаний метод construct тільки-що створеного сервісу, завдання якого - ініціалізація implementation-a (що має тип deadline_timer_service:: implementation_type), який є членом класу basic_io_object і містить деталі реалізації таймера на даній платформі. Але це ще не все:), після виходу з scope-a буде викликаний деструктор таймера, який викличе метод destroy сервісу, як параметр в нього буде переданий timer.implementation. Далі в деструкції io_service-a буде викликаний метод shutdown_service сервісу дедлайн таймерів, а потім видалений екземпляр класу deadline_timer_service. Як я вже згадував, все це заради того, що-б розділити реалізацію та інтерфейс. Як приклад я реалізую клас (хоча насправді їх буде три:), для роботи з повідомленнями windows, який можна буде використовувати разом в бібліотекою boost:: asio. Для початку нам потрібно клас реалізує обробку повідомлень - implementation. Він повинен дозволяти встановлювати обробники повідомлень, видаляти їх, а так-же містити метод для обробки конкретного повідомлення.
Using Namespace Boost:: tuples;
Using Boost:: function;
Class messageloop_impl {
typedef Boost:: function < void ( Const MSG &, boost:: system:: error_code)> handler_type;
typedef Boost:: unordered_map < DWORD , handler_type> table_type ; table_type table_; boost:: asio:: io_service & io_; boost:: asio:: io_service:: work work_; boost:: mutex mutex_;
public : messageloop_impl (boost:: asio:: io_service & io_service): io_ (io_service), work_ (io_service) {}
Void set_handler ( DWORD ID, handler_type handler) {boost:: mutex:: scoped_lock lock (mutex_); table_type:: iterator I;
BOOL contain_handler; tie (i, contain_handler) = find_ (id);
if (contain_handler) runhandler_ (i, MSG (), boost:: asio:: error:: eof); table_.insert (std:: make_pair (id, handler));}
Void process_message ( Const MSG & msg, int & / * out * / processed) {boost:: mutex:: scoped_lock lock (mutex_); table_type:: iterator I;
BOOL contain_handler; tie (i, contain_handler) = find_ (msg.message);
if (contain_handler) {+ + processed ; runhandler_ (i, msg, boost:: system:: error_code ());}}
Void remove_handler ( DWORD ID) {boost:: mutex:: scoped_lock lock (mutex_); table_type:: iterator I;
BOOL contain_handler; tie ( i, contain_handler) = find_ (id);
if (contain_handler) {runhandler_ (i, MSG (), boost:: asio:: error:: eof); table_.erase (i);}}
Void Clear () {boost :: mutex:: scoped_lock lock (mutex_);
for (table_type:: iterator I = table_.begin (); i! = table_.end (); + + i) runhandler_ (i, MSG (), boost:: asio :: error:: eof); table_.clear ();}
private : tuple <table_type:: iterator , bool > find_ ( DWORD ID) {table_type:: iterator I = table_. find (id);
Return tuple <table_type: : iterator , bool > (i, i! = table_.end ());}
Void runhandler_ (table_type:: iterator I, Const MSG & m, boost:: system:: error_code e) {handler_type h = i-> second ; io_.post (boost:: bind (h, m, e));}};
Конструктор messageloop_impl - приймає посилання на io_service і зберігає її всередині об'єкта класу. Обробники повідомлень реалізовані на основі boost:: function, мають сигнатуру void (const MSG &, boost:: system:: error_code). Для зберігання обробників повідомлень використовується хеш таблиця table_. Ф-я set_handler додає обробник в хеш таблицю, ф-я remove_handler відповідно видаляє, метод clear видаляє всі обробники. Метод process_message обробляє повідомлення, у випадку, якщо для даного повідомлення знайдений обробник, значення змінної processed збільшується на одиницю, а обробник передається в io_service за допомогою методу post, далі він викликається методом run, poll або run_one io_service-a. Це потрібно для того, що-б наш обробник повідомлень слідував правилу, згідно з яким усі обробники можуть викликатися тільки в тих потоках, в яких був викликаний метод run (poll або run_one) відповідного io_service-a, або під час дзвінка деструктора io_service-a. Так-же даний клас містить член work_ має тип boost:: asio:: io_service:: work. io_service містить лічильник, який инкрементируется кожен раз, коли починається будь-яка операція, і декрементируется щоразу після завершення чергової операції. Метод run io_service-a не завершується доти, поки цей лічильник не буде дорівнює нулю. Зробивши об'єкт класу io_service:: work членом класу messageloop_impl, ми гарантуємо, що цей лічильник не обнулиться до тих пір, поки хоч один примірник messageloop_impl існує. Перед видаленням обробника, він викликається за другим параметром рівним boost:: asio:: error:: eof. Тепер напишемо код нашого сервісу для обробки повідомлень.
Class basic_messageloop_service: Public Boost:: asio:: io_service:: service {
public :
Static Boost:: asio:: io_service:: id id;
typedef Boost:: shared_ptr <messageloop_impl> implementation_type;
private : boost:: mutex mutex_; std: : Set <implementation_type> processors_; std:: set < DWORD > threads_;
Static Void send_WM_QUIT_to ( DWORD Thread) {
BOOL Result =:: PostThreadMessage (thread, WM_QUIT , 0, 0);
if (! result) {boost:: system :: error_code e = boost:: system:: error_code (:: GetLastError (), boost:: system:: system_category);
Throw Boost:: system:: system_error (e);}}
public :
Explicit basic_messageloop_service (boost:: asio:: io_service & io_service): boost:: asio:: io_service:: service (io_service) {} ~ basic_messageloop_service () {}
Void CONSTRUCT (implementation_type & impl) {impl.reset ( New messageloop_impl (get_io_service ())); boost: : mutex:: scoped_lock lock (mutex_); processors_.insert (impl);}
Void Destroy (implementation_type & impl) {boost:: mutex:: scoped_lock lock (mutex_); processors_.erase (impl); impl.reset (); }
Void shutdown_service () {boost:: mutex:: scoped_lock lock (mutex_); std:: for_each (threads_.begin (), threads_.end (), & basic_messageloop_service:: send_WM_QUIT_to); threads_.clear ();}
template < Class Handler >
Void set_handler (implementation_type & impl, DWORD ID, Handler Handler) {impl-> set_handler (id, handler);}
Void remove_handler (implementation_type & impl, DWORD ID) {impl-> remove_handler (id);}
Void Clear (implementation_type & impl) {impl-> clear ();}
Void Loop () {
MSG MSG;
DWORD thread_id =:: GetCurrentThreadId (); {boost:: mutex:: scoped_lock lock (mutex_); threads_.insert (thread_id);}
while (:: GetMessage (& msg, NULL , 0, 0)) {boost:: mutex:: scoped_lock lock (mutex_);
INT proc_cnt = 0; std:: for_each (processors_.begin (), processors_.end (), boost :: bind (& messageloop_impl:: process_message, _1, msg, boost:: ref (proc_cnt)));
if (proc_cnt == 0):: DispatchMessage (& msg);} {boost:: mutex:: scoped_lock lock (mutex_) ; threads_.erase (thread_id);}}}; boost:: asio:: io_service:: id basic_messageloop_service:: id;
Отже, наш клас повинен бути спадкоємцем класу boost:: asio:: io_service:: service, а так-же мати статичний член id має тип boost:: asio:: io_service:: id, який є унікальним ідентифікатором сервісу. Так-же клас повинен визначати тип implementation_type, в даному випадку це boost:: shared_ptr, Від якого залежить тип змінної класу basic_io_object:: Implementation, тобто реалізації об'єкта обробника повідомлень. Сервіс повинен вміти ініціалізувати об'єкти мають тип implementation_type за допомогою методу construct і деініціалізіровать їх методом destroy. Так-же він містить ряд методів для управління екземплярами класу implementation_type а так-же метод loop, в якому реалізований цикл обробки повідомлень. Наприклад, метод set_handler, він приймає два параметри, перший (impl) має тип implementation_type, другий (id) - ід-р повідомлення і третій (handler) - обробник повідомлення, реалізований просто як виклик impl-> set_handler (id, handler), тоесть просто делегує виклик реалізації. Метод loop, реалізований таким чином, спочатку він запам'ятовує в безлічі threads_ ідентифікатор потоку в якому він викликаний, потім у циклі отримує повідомлення і передає їх обробникам (які сервіс запам'ятовує в безлічі processors_ під час створення кожного з них) а після отримання повідомлення WM_QUIT він видаляє з безлічі thread_ ідентифікатор потоку в якому виконувався. Завдяки цьому можна запустити кілька циклів обробки повідомлень в різних потоках і один обробник зможе отримувати повідомлення з будь-якого. Ну і останнє, метод shutdown_service, повинен привести до завершення всіх циклів обробки повідомлень, тому він просто посилає всім їм повідомлення WM_QUIT. Ну і саме останнє що нам потрібно зробити - реалізувати io_object. Це дуже просто зробити.
template < typename Service>
Class basic_messageloop: Public Boost:: asio:: basic_io_object <Service> {
public :
Explicit basic_messageloop (boost:: asio:: io_service & io_service): boost:: asio:: basic_io_object <Service> (io_service) {}
Void Loop () {
this -> service.loop ();}
template < Class Handler >
Void set_handler ( DWORD ID, Handler Handler) {
this -> service.set_handler ( this -> implementation, id, handler);}
template < Class Handler >
Void set_handler ( LPCTSTR Name, Handler Handler) {
DWORD msg_code =:: RegisterWindowMessage (name);
this -> service.set_handler ( this -> implementation, msg_code, handler);}};
typedef basic_messageloop <basic_messageloop_service> messageloop;
Єдина вимога - клас повинен бути спадкоємцем boost:: asio:: basic_io_object, Де Service - наш сервіс обробників повідомлень. У цьому класі метод set_handler просто викликає метод set_handler свого сервісу і передає в нього свою реалізацію (implementation) та додаткові параметри. Загалом цей клас - просто інтерфейс, він не повинен мати стан (хоча його батько має), а просто перенаправляти всі виклики своєму сервісу. Юзати це можна так:
boost:: asio:: io_service io; asio_aux:: messageloop message_loop1 (io); asio_aux:: messageloop message_loop2 (io); message_loop1.set_handler ( WM_CLOSE , & message_handler); message_loop1.set_handler ( L " MyMessage " , & message_handler); message_loop2. set_handler ( WM_CLOSE , & message_handler); message_loop2.set_handler ( L " MyAnotherMessage " , & message_handler); boost:: thread thr (boost:: bind (& boost:: asio:: io_service:: run, & io)); message_loop1.loop () ;

Багатопоточність: boost.threads

Минулого століття програми, написані з використанням декількох потоків виконання, зустрічалися досить рідко. Сьогодні ж Інтернет-сервери запускають безліч потоків виконання, щоб ефективно обслужити безліч з'єднань з клієнтами. З метою максимізації пропускної здатності сервери транзакцій виконують свої сервіси в окремих потоках. Графічні програми роблять тривалі операції в окремому потоці, продовжуючи чітко реагувати на дії користувача. Цей список можна продовжувати і далі. Деякі програмісти, пишуть програми на C++, використовуючи бібліотеки, надані їх операційними системами і забезпечують системну підтримку потоків. Стандарт POSIX визначає підтримку многопоточ ності в бібліотеці, відомої як PTHREADS. Вона підтримує багатопоточність для широкого діапазону зона операційних систем, включаючи Win32 посредст вом порту pthreads-win32. Однак, це C-бібліотека, вона не реалізує деякі концепції C + +, вона не доступна на деяких платформах. Boost.Threads - бібліотека, призначається для вирішення проблем нестандартності та непереносимості. Boost - це організація, заснована членами робочої групи комітету з стандартизації C++ з ціллю розробки нових бібліотек для C++. На даний час в організацію входять приблизно 2000 членів. Безліч бібліотек можна знайти в дистрибютиві Boost (www.boost.org). Щоб зробити ці бібліотеки безпечними щодо потоків і була створена бібліотеку ка Boost.Threads. Багато експертів C++ внесли свій внесок в дизайн Boost.Threads. Інтерфейс бібліотеки був спроекті ваний з чистого аркуша і не є простою обгорткою якої-небудь багатопотокового API для C. Багато можливостей C++ (такі як існування конструкторів і деструкторів, функціональних об'єктів і шаблонов) були повною мірою використані з метою створення більш гнучкого інтерфейсу. Поточна реалізація працює на платформах POSIX, Win32 і Mac OS X.

Створення потоку

Класс boost::thread представляє потік виконання в тому сенсі, в якому std::fstream представляе файл. Конструктор за замовчуванням створює екземпляр, представляючий поточний потік виконання. Пере-завантажений конструктор приймає функціональнийоб'єкт, що викликається без аргументів і нічого не повертає. Цей конструктор запускає новий потік виконання, який в свою чергу викликає функціональний об'єкт. На перший погляд може здатися, що такий дизайн менш корисний, ніж типовий підхід мови C створення потоку, коли функції, що викликається новимпотоком, передається покажчик на void, за допомогою якого передаються дані. Однак, оскільки бібліотека Boost.Threads замість покажчика на функцію використовує функціональний об'єкт, саме він і може містити дані, необхідні потоку. Такий підхід, насправді, більш гнучкий і типобеспечний. Комбінуючи з функціональними бібліотеціками, такими як Boost.Bind, цей дизайн дозволяєлегко передавати новоствореному потоку будь які обсяги даних. В даний час з об'єктом потоку можна зробити не надто багато. Фактично, можна виконати тільки дві операції. Об'єкти потоків можналегко порівняти на рівність і нерівність, використовуючи операції == і ! =, щоб визначити, чи належать вони до одного і того ж потоку виконання, і можна дочекатися завершення потоку, викликаючи boost::thread::join. Інші потокові бібліотеки дозволяють виконувати над потоками та інші операції (наприклад, встановлювати його пріоритет або навіть зупиняти). Однак так як ці бібліотеки не дуже легко відобразити непереносимі інтерфейси, все ще проводяться дослідження, метою яких є додавання відповідних можливостей і в Boost.Threads. Наступний код демонструє дуже простий приклад використування класу boost::thread. Створюється новий потік, який просто виводить "Hello World" на std::cout, а основний потік чекає його завершення.

#include <boost/thread/thread.hpp>
#include <iostream>

void hello() {
std::cout<<"Hello, World\n";
}

int main() {
boost::thread thrd(&hello);
thrd.join();
}

Мютекси

Кожен, хто писав багатопотокову програму, розуміє, наскільки важливо безлічі потоків не звертатися до одного й того ж ресурсу, в один і той же час. Якщо один потік намагається змінити значення розділяних даних в той час, коли інший потік намагається це значення прочитати, результатом буде невизначена поведінка. Щоб цьому запобігти, використовують деякі спеціальні примітивні типи і операції. Самий фундаментальний з цих типів відомий як мютекс (Скорочення від «mutual exclusion» - взаємне виключення). Мютекс дозволяє отримати доступ до роздільного ресурсу тільки одному потоку в один момент часу, цей потік повинен «заблокувати» мютекс. Якщо інший потік вже заблокував мютекс, то ця операція чекає, поки мютекс не буде звільнений («Розблоковано»), таким чином гарантується, що тільки один потік має доступ до роздільного ресурсу в один момент часу. Мютекс може мати кілька варіацій. Дві великі категорії підтримуваних бібліотекою Boost.Threads мютекс включають прості і рекурсивні мютекси. Простий мютекс може бути заблокований лише один раз. Спроба повторного захоплення мютекса призводить до глухого кута, що виражається в нескінченному очікуванні. При використанні рекурсивного мютекса один потік може заблокувати мьютекс кілька разів і має рівно стільки ж разів його розблокувати, перш ніж інший потік отримає можливість його заблокувати. У рамках цих двох великих категорій мютекс виділяють і інші способи блокування мютекса. Потік може спробувати заблокувати мютекс трьома способами:
  1. Спробувати заблокувати мютекс, очікуючи поки жоден інший потік не буде ним володіти.
  2. Спробувати заблокувати мютекс, негайно але повернувшись, якщо цей мьютекс вже кимось заблокований.
  3. Спробувати заблокувати мютекс, очікуючи або його звільнення іншим потоком, або закінчення зазначеного періоду часу.
Найкращим можливим типом мютекса є рекурсивний мютекс, підтримуючий всі три форми блокування. Однак з кажною варіацією додаються накладні витрати, тому бібліотека Boost.Threads передбачає можливість вибору найбільш ефективного виду мьютекса для кожного конкретного випадку. Таким чином, є шість типів мютексів, перерахованих тут в порядку зниження ефективності:
  • boost::mutex
  • boost::try_mutex
  • boost::timed_mutex
  • boost::recursive_mutex
  • boost::recursive_try_mutex
  • boost::recursive_timed_mutex
Тупики можуть трапитися всякий раз, коли мютекс заблоковано і вчасно не звільнено. Це одна з найпоширеніших помилок, тому бібліотека Boost.Threads спроектована так, щоб зробити її неможливою (або, принаймні, важко реалізуємою). Немає жодної безпосередній операції по блокування або розблокування мютекса. Замість цього класи мьютекс надають визначення ня типів (typedef), які реалізують ідіому RAII (Re- source Acquisition is Initialization - Захоплення ресурсу є ініціалізацією) для блокування і Розблокування мютекса. Ця ідея відома як патерн «Блокування в області видимості» (Scoped Lock). При створенні об'єкту такого типу йому передається посилання на мьютекс. Конструктор блокує мютекс, а деструктор розблокує його. Правила мови C++ гарантують, що деструктор завжди буде викликаний, так що навіть якщо викинуто виключення, мютекс завжди буде коректно розблокований. Цей патерн допомагає гарантувати правильне використання мютекса. Проте, потрібно бути готовим до того, що хоча мютекс виявиться Розблокованим, не факт, що кожен розділяється ресурс опиниться в коректному стані в разі виникнення винятку. Тому, як і у випадку з одним потоком виконання, слід щоразу переконуватися в тому, що виключення не призводять до порушення цілісності стану програми. Крім того, об'єкти блокувань не повинні передаватися іншим потокам, так як їх стан не захищений при такому використанні. У наступному прикладі показано дуже просте використання класу boost::mutex. Створюються два нових потоки, кожний з них 10 раз виводить свій id і лічильник циклу, а основний потік чекає їх завершення. Об'єкт std::cout є ресурсом, тому кожен потік використовує глобальний мютекс, гарантуючий, що тільки один потік в кожен момент часу намагається здійснювати виведення.

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <iostream>
boost::mutex io_mutex;
struct count {
count(int id) : id(id) {}
void operator()() {
for (int i = 0; i < 10; ++i) {
boost::mutex::scoped_lock
lock(io_mutex);
std::cout<<id<< ": "<<i<<std::endl;
}
}
int id;
};
int main(int argc, char* argv[]) {
boost::thread thrd1(count(1));
boost::thread thrd2(count(2));
thrd1.join();
thrd2.join();
return 0;
}
Багато користувачів помітять, що передача даних потоку вимагає ручного кодування функціонального об'єкта. Хоча цей код і тривіальний, писати його кожного разу досить нудно. Є й більш просте рішення. Функціональні бібліотеки дозволяють створити нові функціональні об'єкти, пов'язуючи (bind) інші функціональні об'єкти з даними, які при виклику будуть їм передані. У наступному прикладі показано, як при використанні бібліотеки Boost.Bind можна спростити код попереднього прикладу прикладу, відмовившись від ручного кодування функціонального об'єкта.

/**
  *Ця програма ідентична програмі
  *з попереднього прикладу, крім того, що
  *використовує Boost.Bind
  *при створені потоку,
  *що приймає параметры.
*/
#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>
boost::mutex io_mutex;
void count(int id) {
for (int i = 0; i < 10; ++i) {
boost::mutex::scoped_lock
lock(io_mutex);
std::cout<<id<<": "<<i<<std::endl;
}
}
int main(int argc, char* argv[]) {
boost::thread thrd1(
boost::bind(&count, 1));
boost::thread thrd2(
boost::bind(&count, 2));
thrd1.join();
thrd2.join();
return 0;
}

Умовні змінні

Іноді недостатньо просто встановити блок і скористатися роздільним ресурсом. Іноді необхІдно, щоб цей ресурс перед використанням знаходився в деякому спеціальному стані. Наприклад, потік може намагатися витягти дані з стека, чекаючи, коли в ньому вони з'являться, якщо в цей момент стек порожній. Для реалізації такого виду синхронізації, мьютекса недостатньо. У такому разі можна використовувати інший механізм синхронізації, відомий як умовна змінна. Умовна змінна завжди використовується в зв'язці з мьютексом та ресурсом (або ресурсами). Потік насамперед блокує мьютекс, а потім перевіряє, чи знаходиться ресурс в стані, що дозволяє його безпечне використання необхідним чином. Якщо він не в потрібному стані, потік викликає для умовної змінної операцію очікування. Ця операція призводить до розблокування мьютекса під час очікування, так що інший потік отримує можливість змінити стан роздільного ресурсу. Вона також гарантує, що при поверненні потоку після очікування мьютекс виявиться заблокованим. Коли інший потік змінює стан поділюваного ресурсу, він має повідомити потоки, які можуть чекати умовну змінну, дозволити їм завершити очікування. Наступний приклад ілюструє застосування класу boost::condition. Визначено клас, який реалізує обмежений буфер - контейнер фіксованого розміру з підтримкою введення/виводу в порядку черги (FIFO). Цей буфер зроблений потокобезпечним завдяки boost::mutex. Операції put і get використовують умовну змінну, щоб переконатися в тому, що потік буде чекати, поки буфер не опиниться в стані, необхідному для завершення операції. Створюються два потоки, один поміщає в цей буфер сто цілих, а інший їх же витягує. Обмежений буфер в кожен момент часу може містити тільки 10 цілих, тому кожному з потоків доводиться періодично чекати інший потік. Для перевірки того, що це дійсно відбувається, операції put і get виводять в std::cout діагностичні повідомлення. Нарешті, основний потік чекає завершення обох потоків.

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <iostream>
const int BUF_SIZE = 10;
const int ITERS = 100;
boost::mutex io_mutex;
class buffer
{
public:
typedef boost::mutex::scoped_lock
scoped_lock;

buffer() : p(0), c(0), full(0) {}

void put(int m)
{
scoped_lock lock(mutex);
if (full == BUF_SIZE)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout <<
"Buffer is full. Waiting..."
<< std::endl;
}
while (full == BUF_SIZE)
cond.wait(lock);
}
buf[p] = m;
p = (p+1) % BUF_SIZE;
++full;
cond.notify_one();
}
int get()
{
scoped_lock lk(mutex);
if (full == 0)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout <<
"Buffer is empty. Waiting..."
<< std::endl;
}
while (full == 0)
cond.wait(lk);
}
int i = buf[c];
c = (c+1) % BUF_SIZE;
--full;
cond.notify_one();
return i;
}

private:
boost::mutex mutex;
boost::condition cond;
unsigned int p, c, full;
int buf[BUF_SIZE];
};
buffer buf;
void writer()
{
for (int n = 0; n < ITERS; ++n)
{
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << "sending: "
<< n << std::endl;
}
buf.put(n);
}
}
void reader()
{
for (int x = 0; x < ITERS; ++x)
{
int n = buf.get();
{
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << "received: "
<< n << std::endl;
}
}
}

int main(int argc, char* argv[])
{
boost::thread thrd1(&reader);
boost::thread thrd2(&writer);
thrd1.join();
thrd2.join();
return 0;
}

Локальна пам'ять потоку

Багато функцій не розраховані на рентабельність. Це означає, що небезпечно викликати таку функцію в момент, коли її викликає інший потік. Нереентерабельні функції зберігають статичні дані між успішними викликами або повертають покажчик на статичні дані. Наприклад, функція std::strtok нереентерабельна, оскільки використовує статичні дані для зберігання рядка, що розбивається на токени. Така функція може бути зроблена реєнтерабельним за допомогою двох підходів. Один підхід полягає в зміні її інтерфейсу, так що функція приймає покажчик або посилання на тип даних, який можна використовувати замість використовуваної перш статичної області даних. Наприклад, стандарт POSIX визначає strtok_r, реєнтерабельним версію std::strtok, яка приймає додатковий параметр типу char**, що використовується замість статичної пам'яті. Це просте рішення, до того ж воно забезпечує найбільшу можливу продуктивність, а проте, воно означає зміну відкритого інтерфейсу, що потенційно може призвести до зміни великого обсягу коду. Другий підхід залишає інтерфейс без зміни і замінює статичну пам'ять на локальну пам'ять потоку (thread local storage) (іноді її називають потік-специфічною пам'яттю thread-specific storage). Локальна пам'ять потоку - це дані, пов'язані з конкретним (поточним) потоком. Багатопотокові бібліотеки надають доступ до локальної пам'яті потоку за допомогою інтерфейсу, що дозволяє отримати доступ до екземпляра даних, що належать поточному потоку. Кожен потік отримує свій власний екземпляр цих даних, тобто тут не виникає проблема спільного доступу. Однак, доступ до локальної пам'яті потоку повільніший, ніж до локальних або статичних данних, так що це не завжди найкраще рішення. Тим не менш, це єдине можливе рішення в разі, коли зміна відкритого інтерфейсу неприйнятна. Бібліотека Boost.Threads забезпечує доступ до локальної пам'яті потоку за допомогою «розумного» покажчика (smart pointer) boost::thread_specific_ptr. При першому зверненні кожного потоку до цього «розумному» вказівником, він має нульове значення, так що код повинен перевірити цей факт і забезпечити ініціалізацію покажчика при першому використанні. Бібліотека Boost.Threads гарантує, що дані, які зберігаються в локальній пам'яті потоку, будуть «зачищені» при виході з потоку. Наступний приклад ілюструє дуже просте застосування класу boost::thread_specific_ptr. Створюються два потоки, в них ініціалізується локальна пам'ять потоку, а потім у циклі 10 разів значення цілого, розташованого за адресою «розумного» покажчика інкрементується, а результат виводиться в std::cout (який синхронізований за допомогою мьютекса, так як є роздільним ресурсом) . Основний потік чекає завершення цих двох потоків. Висновок в цьому прикладі ясно показано, що кожен потік оперує зі своїм екземпляром даних, хоча обидва вони використовують один і той же boost::thread_specific_ptr.

#include <boost/thread/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/tss.hpp>
#include <iostream>
boost::mutex io_mutex;
boost::thread_specific_ptr<int> ptr;
struct count
{
count(int id) : id(id) { }
void operator()()
{
if (ptr.get() == 0)
ptr.reset(new int(0));
for (int i = 0; i < 10; ++i)
{
(*ptr)++;
boost::mutex::scoped_lock
lock(io_mutex);
std::cout << id << ": "
<< *ptr << std::endl;
}
}
int id;
};
int main(int argc, char* argv[])
{
boost::thread thrd1(count(1));
boost::thread thrd2(count(2));
thrd1.join();
thrd2.join();
return 0;
}

Одноразові функції

Залишається розібратися з одним питанням: як зробити функції ініціалізації (такі як конструктори) потокобезпечна. Наприклад, коли «глобальний» екземпляр об'єкта створюється як сінглетон рівня програми (єдиний існуючий з програми такого типу), існує проблема порядку інстанціюванія, тому використовується функція, яка повертає статичний екземпляр, яка гарантує, що при першому зверненні до неї цей екземпляр буде створений. Проблема в тому, що якщо кілька потоків одночасно викличуть цю функцію, конструктор для статичного об'єкта також може бути викликаний кілька разів, і результати можуть виявитися поганими. Рішення проблеми в так званих «одноразові функції» (once routine). Така функція викликається в програмі тільки один раз. Якщо кілька потоків спробують її викликати одночасно, тільки один з них отримає таку можливість, а в цей час всі інші потоки будуть чекати, поки виконання функції не завершиться. Щоб гарантувати однократне виконання, така функція викликається побічно через іншу функцію, якій передається покажчик на вихідну і посилання на спеціальний прапор (flag), що сигналізує про факт виклику функції. Цей прапор ініціалізується статично, що гарантує ініціалізацію в період компіляції, а не в період виконання. І таким чином не представляє проблеми для багатопотокової ініціалізації. Бібліотека Boost.Threads надає можливість одноразового виклику функції за допомогою boost::call_once, а також визначає тип для прапора boost::once_flag і спеціальну макропідстановку, використовувану для статичної ініціалізації прапора, BOOST_ONCE_INIT. Наступний приклад показує дуже простий приклад використання boost::call_once. Глобальне ціле статично ініціалізується нулем, а примірник boost::once_flag статично ініціалізується за допомогою BOOST_ONCE_INIT. Основний потік запускає два потоки, кожен з яких намагається «ініціалізувати» глобальне ціле, викликаючи boost::call_once з покажчиком на функцію, інкрементуючу ціле. Потім основний потік чекає завершення обох потоків і виводить кінцеве значення цілого в std::cout. Висновок демонструє, що функція дійсно була викликана тільки одного разу, так як значення цілого - одиниця.

#include <boost/thread/thread.hpp>
#include <boost/thread/once.hpp>
#include <iostream>
int i = 0;
boost::once_flag flag = BOOST_ONCE_INIT;
void init() {
++i;
}
void thread() {
boost::call_once(&init, flag);
}
int main(int argc, char* argv[]) {
boost::thread thrd1(&thread);
boost::thread thrd2(&thread);
thrd1.join();
thrd2.join();
std::cout << i << std::endl;
return 0;
}