Коль уж встал вопрос о кросс-платформенной многопоточности, и коль уж новый стандарт языка её поддерживает, стоит иметь под рукой пример её использования:
Классика жанра: "Обедающие философы"
#include <cstdlib>
#include <ctime>
#include <exception>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;
// FORK ////////////////////////////////////////////////////////////////////////
class Fork
{
friend bool tryToTake(Fork &fork);
friend void release(Fork &fork);
private:
timed_mutex _mutex;
int _number;
public:
Fork(const int number) :
_mutex(),
_number(number)
{}
int number() const
{
return _number;
}
};
bool tryToTake(Fork &fork)
{
return fork._mutex.try_lock_for(chrono::milliseconds(0));
}
void release(Fork &fork)
{
fork._mutex.unlock();
}
// PHILOSOPHIER ////////////////////////////////////////////////////////////////
class Philosophier
{
private:
class Implementation
{
private:
enum State
{
Eat,
Relax,
Wait
};
Fork &_leftFork;
Fork &_rightFork;
int _number;
volatile bool _finalize;
State _state;
bool _tryToTake(Fork &fork)
{
bool result = tryToTake(fork);
if(result)
{
cout << "Philosophier " << _number << " has taken fork " << fork.number() << "." << endl;
}
else
{
cout << "Philosophier " << _number << " hasn't taken fork " << fork.number() << "." << endl;
}
return result;
}
void _release(Fork &fork)
{
release(fork);
cout << "Philosophier " << _number << " has released fork " << fork.number() << "." << endl;
}
void _action(const int timeout)
{
switch(_state)
{
case Relax:
case Wait:
{
bool leftResult = _tryToTake(_leftFork);
bool rightResult = _tryToTake(_rightFork);
if(leftResult && rightResult)
{
cout << "Philosophier " << _number << " is eating " << timeout << " times." << endl;
_state = Eat;
}
else
{
if(leftResult)
{
_release(_leftFork);
}
if(rightResult)
{
_release(_rightFork);
}
cout << "Philosophier " << _number << " is waiting " << timeout << " times." << endl;
_state = Wait;
}
}
break;
case Eat:
{
_release(_leftFork);
_release(_rightFork);
cout << "Philosophier " << _number << " is relaxed " << timeout << " times." << endl;
_state = Relax;
}
break;
default:
throw exception();
}
}
public:
Implementation(const int number, Fork &leftFork, Fork &rightFork) :
_number(number),
_leftFork(leftFork),
_rightFork(rightFork),
_finalize(false),
_state(Relax)
{}
void operator()()
{
cout << "Philosophier " << _number << " began." << endl;
while(!_finalize)
{
int timeout = rand() % 5 + 1;
_action(timeout);
this_thread::sleep_for(chrono::seconds(timeout));
}
cout << "Philosophier " << _number << " ended." << endl;
}
void finalize()
{
_finalize = true;
}
};
Implementation _implementation;
thread _thread;
public:
Philosophier(const int number, Fork &leftFork, Fork &rightFork) :
_implementation(number, leftFork, rightFork),
_thread(ref(_implementation))
{}
~Philosophier()
{
_implementation.finalize();
_thread.join();
}
};
// MAIN ////////////////////////////////////////////////////////////////////////
int main()
{
srand(time(NULL));
cout << "Press Enter for exit." << endl;
vector<Fork *> forks;
vector<Philosophier *> philosophiers;
const int n = 3;
Fork *firstFork = new Fork(1), *previousFork = firstFork;
forks.push_back(previousFork);
for(int i = 0; i < n; ++i)
{
Fork *nextFork;
if(i < n - 1)
{
nextFork = new Fork(i + 2);
forks.push_back(nextFork);
}
else
{
nextFork = firstFork;
}
Philosophier *philosophier = new Philosophier(i + 1, *previousFork, *nextFork);
philosophiers.push_back(philosophier);
previousFork = nextFork;
}
getchar();
for(int i = 0; i < philosophiers.size(); ++i)
{
delete philosophiers[i];
}
for(int i = 0; i < forks.size(); ++i)
{
delete forks[i];
}
return 0;
}
Вкратце основные положения:
- Запуск потока происходит путём создания объекта класса std::thread (файл <thread>).
- В качестве тела потока (и аргумента конструктора thread) может выступать любой функтор: как обычная функция, так и объект с перегруженными оператором (). Функтор может иметь произвольную сигнатуру - любое количество параметров любых типов, библиотека это успешно переваривает. Аргументы функтора передаются как опциональные параметры thread сразу после самого функтора.
- (Важно!) Все аргументы thread - как сам функтор, если это объект, так и передаваемые функтору аргументы - передаются по значению, а не по ссылке. Поэтому всегда происходит полное копирование всех переданных аргументов в контекст нового потока. Если в качестве функтора нужно передать конкретный объект, а не его копию, функтор нужно обернуть в объект std::ref() - см. в коде.
- Синхронизация потоков происходит при вызове у потока метода join() - дожидаемся завершения интересующего нас потока и двигаемся дальше. Любопытна трансляция исключений. Так, если поток умер по некоему необработанному исключению, это будет злопамятно сохранено, и тот поток, который вызовет join - тот и получит это исключение от умершего потока. Так что join надо вызывать с опаской, чтобы не получить по лбу.
- Поток можно сделать зомби, вызвав у объекта потока метод detach. А всё потому, что нет сборщика мусора. Поэтому деструктор объекта thread принудительно убивает поток. Поэтому локальная переменная потока, выходящая из области видимости, приводит к срыву работы. Чтобы этого не происходило, нужно делать detach. Извращение. Я бы заставил хранить объект потока до победы, ибо нечего зомби разводить в системе - считай, аналог утечки памяти.
- Глобальная переменная std::this_thread есть объект текущего потока в контексте каждого потока. Кстати говоря, появилось служебное слово thread_local для переменных, которые не локальные, но и не глобальные как static. Т.е. глобальны в контексте потока. Это ещё одно извращение, поскольку всегда можно обеспечить нужный уровень локальности соответствующим объектом.
- У std::this_thread есть метод sleep_for для засыпания потока на таймаут. Вообще, впечатление такое, что в общем случае методы *_for работают с интервалами времени, а методы *_until работают до наступления определённого момента календарного времени. Но в подробности я ещё не погружался.
- Есть некий элемент std::chrono (даже не знаю, класс это или пространство имён), в котором сосредоточены всякие разности для работы со временем. Так chrono::milliseconds и chrono::seconds описывают интервалы времени в соответствующих единицах. Но там есть и точки календарного времени и прочее всякое разное.
- Объекты синхронизации представлены разными мьютексами и условными переменными (файл <mutex>). Но поскольку в подавляющем большинстве случаев достаточно мьютекса (как семафора со счётчиком 1), на нём и остановимся.
- Есть в доску простой std::mutex с методами lock и unlock. Тут и комментировать особо нечего.
- Есть мьютекс с таймаутом std::timed_mutex, у которого в дополнение к простому мьютексу есть методы try_lock_for и try_lock_until с аргументами из chrono с вышеописанной смысловой разницей. Возвращают эти методы bool, означающий успех или неуспех блокировки.
- Есть всякие другие мьютексы, и возможности установки/снятия блокировки по bool-выражению, чтобы исключить гонку. Есть объекты для критических секций. Есть возможность блокировки групп мьютексов. Но до этого руки ещё не дошли.