Семинар 8. Параллельное программирование на MPI (часть 1)
Лекция 8: Многопоточное программирование: Intel Threading Building Blocks
1. Лекция 8:
Intel Threading Building Blocks
(Multithreading programming)
Курносов Михаил Георгиевич
к.т.н. доцент Кафедры вычислительных систем
Сибирский государственный университет
телекоммуникаций и информатики
http://www.mkurnosov.net
2. Intel Threading Building Blocks
Intel Treading Building Blocks (TBB) –
это кроссплатформенная библиотека шаблонов
C++ для создания многопоточных программ
История развития:
o
o
o
o
o
o
o
o
2006 – Intel TBB v1.0 (Intel compiler only)
2007 – Intel TBB v2.0 (Open Source, GPLv2)
2008 – Intel TBB v2.1 (thread affinity, cancellation)
2009 – Intel TBB v2.2 (C++0x lambda functions)
…
2011 – Intel TBB v4.0
2012 – Intel TBB v4.1
2013 – Intel TBB v.4.2
http://threadingbuildingblocks.org
2
3. Intel Threading Building Blocks
Open Source Community Version GPL v2
Поддерживаемые операционные системы:
o Microsoft Windows {XP, 7, Server 2008, …}
o GNU/Linux + Android
o Apple Mac OS X 10.7.4, …
http://threadingbuildingblocks.org
3
6. Intel Threading Building Blocks
Intel TBB позволяет абстрагироваться от низкоуровневых
потоков и распараллеливать программу в терминах
параллельно выполняющихся задач (task parallelism)
Задачи TBB “легче” потоков операционной системы
Планировщик TBB использует механизм “work stealing”
для распределения задач по потокам
Все компоненты Intel TBB определены в пространстве
имен C++ (namespace) “tbb”
6
7. Компиляция программ с Intel TBB
GNU/Linux
$ g++ –Wall –o prog ./prog.cpp –ltbb
Microsoft Windows (Intel C++ Compiler)
C:> icl /MD prog.cpp tbb.lib
7
10. Компиляция и запуск tbb_hello
$ g++ -Wall -I~/opt/tbb/include
-L~/opt/tbb/lib
-o tbb_hello
./tbb_hello.cpp -ltbb
$ ./tbb_hello
Hello from task 2
Hello from task 1
10
11. Инициализация библиотеки
Любой поток использующий алгоритмы или планировщик TBB
должен иметь инициализированный объект
tbb::task_scheduler_init
TBB >= 2.2 автоматически инициализирует планировщик
Явная инициализация планировщика позволяет:
управлять когда создается и уничтожается планировщик
устанавливать количество используемых потоков
выполнения
устанавливать размер стека для потоков выполнения
11
12. Инициализация библиотеки
Явная инициализация планировщика
#include <tbb/task_scheduler_init.h>
int main()
{
tbb::task_scheduler_init init;
return 0;
}
12
13. Инициализация библиотеки
Конструктор класса task_scheduler_init принимает
два параметра:
task_scheduler_init(int max_threads = automatic,
stack_size_type thread_stack_size = 0);
Допустимые значения параметра max_threads:
task_scheduler_init::automatic –
количество потоков определяется автоматически
task_scheduler_init::deferred –
инициализация откладывается до явного вызова метода
task_scheduler_init::initialize(max_threads)
Положительное целое – количество потоков
13
14. Инициализация библиотеки
#include <iostream>
#include <tbb/task_scheduler_init.h>
int main()
{
int n = tbb::task_scheduler_init::default_num_threads();
for (int p = 1; p <= n; ++p) {
// Construct task scheduler with p threads
tbb::task_scheduler_init init(p);
std::cout << "Is active = " << init.is_active()
<< std::endl;
}
return 0;
}
14
16. parallel_for
void saxpy(float a, float *x, float *y, size_t n)
{
for (size_t i = 0; i < n; ++i)
y[i] += a * x[i];
}
parallel_for позволяет разбить пространство итерации
на блоки (chunks), которые обрабатываются разными
потоками
Требуется создать класс, в котором перегруженный
оператор вызова функции operator() содержит код
обработки блока итераций
16
18. parallel_for
int main()
{
float a = 2.0;
float *x, *y;
size_t n = 100000000;
x = new float[n];
y = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();
task_scheduler_init init(4);
parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());
tick_count t1 = tick_count::now();
cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;
delete[] y;
return 0;
}
18
19. parallel_for
int main()
{ Класс blocked_range(begin, end, grainsize) описывает одномерное
float a = 2.0;
пространство итераций
float *x, *y;
size_t n = 100000000;
В Intel TBB доступно описание многомерных пространств итераций
(blocked_range2d, ...)
x = new float[n];
y = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();
task_scheduler_init init(4);
parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());
tick_count t1 = tick_count::now();
cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;
delete[] y;
return 0;
}
19
20. affinity_partitioner
int main()
{
// ...
static affinity_partitioner ap;
parallel_for(blocked_range<size_t>(0, n),
saxpy_par(a, x, y), ap);
// ...
return 0;
}
Класс affinity_partitioner запоминает какими потоками
выполнялись предыдущие итерации и пытается
распределять блоки итераций с учетом этой информации
– последовательные блоки назначаются на один и тот же
поток для эффективного использования кеш-памяти
20
21. parallel_for (C++11 lambda expressions)
int main()
{
// ...
x = new float[n];
y = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();
parallel_for(blocked_range<size_t>(0, n),
[=](const blocked_range<size_t>& r) {
for (size_t i = r.begin(); i != r.end(); ++i)
y[i] += a * x[i];
}
);
tick_count t1 = tick_count::now();
cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;
delete[] y;
return 0;
}
21
22. parallel_for (C++11 lambda expressions)
int main()
Анонимная функция (лямбда-функция, С++11)
{
[=] – захватить все автоматические переменные
// ...
(const blocked_range …) – аргументы функции
x = new float[n];
{ ... } – код функции
y = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();
parallel_for(blocked_range<size_t>(0, n),
[=](const blocked_range<size_t>& r) {
for (size_t i = r.begin(); i != r.end(); ++i)
y[i] += a * x[i];
}
);
tick_count t1 = tick_count::now();
cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;
delete[] y;
return 0;
}
22
23. parallel_reduce
float reduce(float *x, size_t n)
{
float sum = 0.0;
for (size_t i = 0; i < n; ++i)
sum += x[i];
return sum;
}
parallel_reduce позволяет распараллеливать циклы
и выполнять операцию редукции
23
24. parallel_reduce
class reduce_par {
public:
float sum;
void operator()(const blocked_range<size_t> &r)
{
float sum_local = sum;
float *xloc = x_;
size_t end = r.end();
for (size_t i = r.begin(); i != end; ++i)
sum_local += xloc[i];
sum = sum_local;
}
// Splitting constructor: вызывается при порождении новой задачи
reduce_par(reduce_par& r, split): sum(0.0), x_(r.x_) {}
// Join: объединяет результаты двух задач (текущей и r)
void join(const reduce_par& r) {sum += r.sum;}
reduce_par(float *x): sum(0.0), x_(x) {}
private:
float *x_;
};
24
26. parallel_sort
void parallel_sort(RandomAccessIterator begin,
RandomAccessIterator end,
const Compare& comp);
parallel_sort позволяет упорядочивать последовательности
элементов
Применяется детерминированный алгоритм
нестабильной сортировки с трудоемкостью O(nlogn) –
алгоритм не гарантирует сохранения порядка следования
элементов с одинаковыми ключами
26
27. parallel_sort
#include <cstdlib>
#include <tbb/parallel_sort.h>
using namespace std;
using namespace tbb;
int main()
{
size_t n = 10;
float *x = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = static_cast<float>(rand()) / RAND_MAX * 100;
parallel_sort(x, x + n, std::greater<float>());
delete[] x;
return 0;
}
27
28. Планировщик задач (Task scheduler)
Intel TBB позволяет абстрагироваться от реальных
потоков операционной системы и разрабатывать
программу в терминах параллельных задач
(task-based parallel programming)
Запуск TBB-задачи примерно в 18 раз быстрее запуска
потока POSIX в GNU/Linux (в Microsoft Windows
примерно в 100 раз быстрее)
В отличии от планировщика POSIX-потоков в GNU/Linux
планировщик TBB реализует “не справедливую” (unfair)
политику распределения задач по потокам
28
29. Числа Фибоначчи: sequential version
int fib(int n)
{
if (n < 2)
return n;
return fib(n - 1) + fib(n - 2);
}
29
30. Числа Фибоначчи: parallel version
int fib_par(int n)
{
int val;
fibtask& t = *new(task::allocate_root()) fibtask(n, &val);
task::spawn_root_and_wait(t);
return val;
}
allocate_root выделяет память под корневую задачу (task) класса fibtask
spawn_root_and_wait запускает задачу на выполнение и ожидает её
завершения
30
31. Числа Фибоначчи: parallel version
class fibtask: public task {
public:
const int n;
int* const val;
fibtask(int n_, int* val_): n(n_), val(val_) {}
};
task* execute()
{
if (n < 10) {
*val = fib(n);
// Use sequential version
} else {
int x, y;
fibtask& a = *new(allocate_child()) fibtask(n - 1, &x);
fibtask& b = *new(allocate_child()) fibtask(n - 2, &y);
// ref_count: 2 children + 1 for the wait
set_ref_count(3);
spawn(b);
spawn_and_wait_for_all(a);
spawn запускает задачу на выполнение
*val = x + y;
и не ожидает её завершения
}
spawn_and_wait_for_all – запускает
return NULL;
задачу и ожидает завершения всех
}
дочерних задач
31
32. Числа Фибоначчи: parallel version
int main()
{
int n = 42;
tick_count t0 = tick_count::now();
int f = fib_par(n);
tick_count t1 = tick_count::now();
cout << "Fib = " << f << endl;
cout << "Time: " << std::fixed << (t1 - t0).seconds()
<< " sec." << endl;
return 0;
}
32
33. Граф задачи (Task graph)
Task A
Depth = 0
Refcount = 2
Task B
Task E
Depth = 1
Depth = 1
Refcount = 2
Refcount = 0
Task C
Task D
Depth = 2
Depth = 2
Refcount = 0
Refcount = 0
33
34. Планирование задач (Task scheduling)
Каждый поток поддерживает дек готовых к выполнению
задач (deque, двусторонняя очередь)
Планировщик использует комбинированный алгоритма
на основе обход графа задач в ширину и глубину
Top:
Oldest task
Bottom:
Youngest
Task
Task E
Task D
34
35. Планирование задач (Task scheduling)
Листовые узлы в графе задач – это задачи готовые
к выполнению (ready task, они не ожидают других)
Потоки могу захватывать (steal) задачи из чужих деков
(с их верхнего конца)
Top:
Oldest task
Bottom:
Youngest
Task
Top:
Oldest task
Task E
Task D
Bottom:
Youngest
Task
35
36. Выбор задачи из дека
Задача для выполнения выбирается одним из следующих
способов (в порядке уменьшения приоритета):
1. Выбирается задача, на которую возвращен указатель
методом execute предыдущей задачи
2. Выбирается задача с нижнего конца (bottom) дека потока
3. Выбирается первая задача из дека (с его верхнего конца)
случайно выбранного потока – work stealing
36
37. Помещение задачи в дек потока
Задачи помещаются в дек с его нижнего конца
В дек помещается задача порожденная методом spawn
Задача может быть направлена на повторное выполнение
методом task::recycle_to_reexecute
Задача имеет счетчик ссылок (reference count)
равный нулю – все дочерние задачи завершены
37
38. Потокобезопасные контейнеры
Intel TBB предоставляет классы контейнеров
(concurrent containers), которые корректно могут
обновляться из нескольких потоков
Для работы в многопоточной программе со стандартными
контейнерами STL доступ к ним необходимо защищать
блокировками (мьютексами)
Особенности Intel TBB:
o при работе с контейнерами применяет алгоритмы
не требующие блокировок (lock-free algorithms)
o при необходимости блокируются лишь небольшие
участки кода контейнеров (fine-grained locking)
38
41. Взаимные исключения (Mutual exclusion)
Взаимные исключения (mutual exclusion) позволяют
управлять количеством потоков, одновременно
выполняющих заданный участок кода
В Intel TBB взаимные исключения реализованы
средствами мьютексов (mutexes) и блокировок (locks)
Мьютекс (mutex) – это объект синхронизации,
который в любой момент времени может быть захвачен
только одним потоком, остальные потоки ожидают его
освобождения
41
42. Свойства мьютексов Intel TBB
Scalable
Fair – справедливые мьютексы захватываются в порядке
обращения к ним потоков (даже если следующий поток
в очереди находится в состоянии сна; несправедливые
мьютексы могут быть быстрее)
Recursive – рекурсивные мьютексы позволяют
потоку захватившему мьютекс повторно его получить
Yield – при длительном ожидании мьютекса поток
периодически проверяет его текущее состояние и снимает
себя с процессора (засыпает, в GNU/Linux вызывается
sched_yield(), а в Microsoft Windows – SwitchToThread())
Block – потока освобождает процессор до тех пор, пока
не освободится мьютекс (такие мьютексы рекомендуется
использовать при длительных ожиданиях)
42
43. Мьютексы Intel TBB
spin_mutex – поток ожидающий освобождения мьютекса
выполняет пустой цикл ожидания (busy wait)
spin_mutex рекомендуется использовать для защиты
небольших участков кода (нескольких инструкций)
queuing_mutex – scalable, fair, non-recursive, spins in user space
spin_rw_mutex – spin_mutex + reader lock
mutex и recursive_mutex – это обертки
вокруг взаимных исключений операционной системы
(Microsoft Windows – CRITICAL_SECTION,
GNU/Linux – мьютексы библиотеки pthread)
43
44. Мьютексы Intel TBB
Mutex
Scalable
Fair
Recursive
Long
Wait
Size
mutex
OS dep.
OS dep.
No
Blocks
>= 3
words
recursive_mutex
OS dep.
OS dep.
Yes
Blocks
>= 3
words
spin_mutex
No
No
No
Yields
1 byte
queuing_mutex
Yes
Yes
No
Yields
1 word
spin_rw_mutex
No
No
No
Yields
1 word
queuing_rw_mutex
Yes
Yes
No
Yields
1 word
44
45. spin_mutex
ListNode *FreeList;
spin_mutex ListMutex;
ListNode *AllocateNode()
{
ListNode *node;
{
// Создать и захватить мьютекс (RAII)
spin_mutex::scoped_lock lock(ListMutex);
node = FreeList;
if (node)
FreeList = node->next;
} // Мьютекс автоматически освобождается
if (!node)
node = new ListNode()
return node;
}
45
46. spin_mutex
void FreeNode(ListNode *node)
{
spin_mutex::scoped_lock lock(ListMutex);
node->next = FreeList;
FreeList = node;
}
Конструктор scoped_lock ожидает освобождения мьютекса ListMutex
Структурный блок (операторные скобки {}) внутри AllocateNode
нужен для того, чтобы при выходе из него автоматически вызывался
деструктор класса scoped_lock, который освобождает мьютекс
Программная идиома RAII – Resource Acquisition Is Initialization
(получение ресурса есть инициализация)
46
47. spin_mutex
ListNode *AllocateNode()
{
ListNode *node;
spin_mutex::scoped_lock lock;
lock.acquire(ListMutex);
node = FreeList;
if (node)
FreeList = node->next;
lock.release();
if (!node)
node = new ListNode();
return node;
}
Если защищенный блок (acquire-release) сгенерирует
исключение, то release вызван не будет!
Используйте RAII если в пределах критической секции
возможно возникновение исключительной ситуации
47
48. Атомарные операции (Atomic operations)
Атомарная операция (Atomic operation) – это операций,
которая в любой момент времени выполняется только
одним потоком
Атомарные операции намного “легче” мьютексов –
не требуют блокирования потоков
TBB поддерживаем атомарные переменные
atomic<T> AtomicVariableName
48
49. Атомарные операции (Atomic operations)
Операции над переменной atomic<T> x
=x
- чтение значения переменной x
x=
- запись в переменную x значения и его возврат
x.fetch_and_store(y)
x = y и возврат старого значения x
x.fetch_and_add(y)
x += y и возврат старого значения x
x.compare_and_swap(y, z)
если x = z, то x = y, возврат старого значения x
49
50. Атомарные операции (Atomic operations)
atomic<int> counter;
unsigned int GetUniqueInteger()
{
return counter.fetch_and_add(1);
}
50
51. Атомарные операции (Atomic operations)
atomic<int> Val;
int UpdateValue()
{
do {
v = Val;
newv = f(v);
} while(Val.compare_and_swap(newv, v) != v);
return v;
}
51
52. Аллокаторы памяти
Intel TBB предоставляет два аллокатора
памяти (альтернативы STL std::allocator)
scalable_allocator<T> – обеспечивает параллельное
выделение памяти нескольким потокам
cache_aligned_allocator<T> – обеспечивает выделение
блоков памяти, выравненных на границу длины кешлинии (cacheline)
Это позволяет избежать ситуации когда потоки на разных
процессорах пытаются модифицировать разные слова
памяти, попадающие в одну строку кэша, и как следствие,
постоянно перезаписываемую из памяти в кеш
52
53. Аллокаторы памяти
/* STL vector будет использовать аллокатор TBB */
std::vector<int, cache_aligned_allocator<int> > v;
53
54. Ссылки
James Reinders. Intel Threading Building Blocks.
– O'Reilly, 2007. – 336p.
Intel Threading Building Blocks Documentation //
http://software.intel.com/sites/products/documentation/docli
b/tbb_sa/help/index.htm
54