Недавно я опять столкнулся с чудесным языком C++ и одной из его реализаций многопоточности, а именно с OpenMP. На самом деле, ощущение у меня осталось двоякое. С одной стороны, после Python'а с его фактическим отсутствием потоков, возможность хоть как-то с ними работать должна была вызвать ощущение счастья. Но все портили мои пол года использования связки Scala+Akka, которые позволили забыть работу с потоками-процессами на низком уровне как страшный сон. OpenMP занимает промежуточное положение - с одной стороны, разработчику доступны некоторые абстракции по сравнению с PThread, но эти абстракции недостаточно высокоуровневые, если смотреть с высоты акторов из Akka.

OpenMP - это, на самом деле, стандарт, описывающий множество директив и библиотечных функций, призванных облегчить разработку многопоточных приложений с разделяемой памятью. Компиляторы же могут этот стандарт поддерживать, а могут и нет (GCC и стандартный компилятор VS поддерживают). Набор предоставляемых инструментов больше всего подходит для организации работы по схеме master-workers: главный поток выполняет инструкции, пока не встретит параллельный фрагмент. После этого он создает worker-потоки, распределяет задания, работает над заданиями вместе с ними, пока все они не дойдут до момента синхронизации. Затем все повторяется.

Главной плюшкой OpenMP являет возможность постепенного добавления поддержки многопоточности в уже написанную программу практически без изменений кода. Самый тупой и при этом самый популярный способ - это распареллеливание циклов: достаточно написать #pragma omp parallel for прямо перед ним. Разумеется, добавив вначале #include <omp.h>. Примеров такого подхода достаточно много и в гугле, и на гитхабе, но лучше всего сразу же посмотреть документацию. Например, вот тут (по-русски). Или сразу же спецификацию.

На самом деле, схемой master-worker дело не ограничивается. Я попробовал смоделировать задачу обедающих философов. На C++ я пишу не слишком хорошо, но код все равно выложу в качестве примера странного использования OpenMP.

Задача звучит достаточно просто: имеется круглый стол, тарелка спагетти, пять философов вокруг стола и 5 вилок, распределенных между каждой парой соседей-философов. При этом философ может находится в двух состояниях - философ есть и философ думает. Философы народ странный и могут есть только в том случае, если возьмут в руки сразу две вилки (правую и левую). Когда философ не ест - он думает. Требуется смоделировать задачу таким образом, чтобы ни один философ не голодал (вечно переходил из состояния "думает" в "ест" и наоборот) при условии, что философы не могут общаться друг с другом.

Самый простой способ добиться отсутствия взаимоблокировок - присвоить каждой вилке номер (0 до 5) и ввести правило: философ сначала берет вилку с меньшим номером, а затем с большим. Класть же вилки на стол он должен в обратном порядке - сначала с большим номером, а затем с меньшим. Т.е для первых четверых философов алгоритм будет следующим - сначала берется правая вилка, затем левая. А вот для пятого все меняется: сначала берется левая, а затем правая. Такой подход не даст пятому философы взять последнюю оставшуюся на столе вилку, если остальные уже разобрали, и ее сможет взять первый философ, что позволит ему приступить к еде а затем вилки освободить. Этот способ, конечно, далеко не эффективен, но очень прост в реализации и понимании.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include "omp.h"


#define PHILO_COUNT 5
#define FORKS_COUNT 5

omp_lock_t forks[FORKS_COUNT];

void think(int i) {
  int secs = 1 + (rand() % PHILO_COUNT);
  printf("Философ %d думает %d секунд...\n", i, secs);
  sleep(secs);
}

void eat(int i) {
  int fork_first;
  int fork_second;

  if (i == PHILO_COUNT - 1) {
    fork_first = 0;
    fork_second = i;
  } else {
    fork_first = i;
    fork_second = i + 1;
  }

  printf("Философ %d просит вилку (%d)\n", i, fork_first);
  omp_set_lock(&forks[fork_first]);

  printf("Философ %d просит вилку (%d)\n", i, fork_second);
  omp_set_lock(&forks[fork_second]);

  //симуляция процесса поедания пищи
  int secs = rand() % PHILO_COUNT;
  printf("Философ %d ест %d секунд\n...", i, secs);

  omp_unset_lock(&forks[fork_second]);
  omp_unset_lock(&forks[fork_first]);
}

void simulation(int i) {
  while (true) {
      think(i);
      eat(i);
  }
}

int main() {

  srand(time(NULL));
  omp_set_num_threads(PHILO_COUNT);

  for (int i = 0; i < PHILO_COUNT; i++) {
    omp_init_lock(&forks[i]);
  }

  #pragma omp parallel for private(i)
  for (int i = 0; i < PHILO_COUNT; i++) {
    simulation(i);
  }

  return 0;
}

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


Comments

comments powered by Disqus