Это немного поздно, но, надеюсь, этот пример кода поможет другим в аналогичной ситуации!
Как упоминалось в osgx, OpenMP ничего не говорит о сигналах, но, поскольку OpenMP часто реализуется с pthreads в системах POSIX, мы можем использовать подход pthread signal.
Для тяжелых вычислений с использованием OpenMP, скорее всего, есть лишь несколько мест, где вычисления можно безопасно остановить. Поэтому в случае, когда вы хотите получить преждевременные результаты, мы можем использовать синхронную обработку сигналов, чтобы безопасно сделать это. Дополнительным преимуществом является то, что это позволяет нам принимать сигнал от определенного потока OpenMP (в приведенном ниже примере кода мы выбираем главный поток). При обнаружении сигнала мы просто устанавливаем флаг, указывающий, что вычисления должны быть остановлены. Затем каждый поток должен периодически проверять этот флаг, когда это удобно, а затем завершать свою долю рабочей нагрузки.
Используя этот синхронный подход, мы позволяем вычислениям завершаться изящно и с минимальными изменениями в алгоритме. С другой стороны, желаемый подход с обработчиком сигналов может быть неприемлемым, поскольку, вероятно, будет сложно сопоставить текущие рабочие состояния каждого потока в согласованный результат. Однако одним недостатком синхронного подхода является то, что вычисления могут занять значительное время, прежде чем они остановятся.
Устройство проверки сигналов состоит из трех частей:
- Блокировка соответствующих сигналов. Это должно быть сделано за пределами области
omp parallel
, чтобы каждый поток OpenMP (pthread) наследовал такое же поведение блокировки.
- Опрос желаемых сигналов от главного потока. Для этого можно использовать
sigtimedwait
, но некоторые системы (например, MacOS) этого не поддерживают. Более переносимо, мы можем использовать sigpending
для опроса любых заблокированных сигналов, а затем дважды проверить, являются ли заблокированные сигналы тем, что мы ожидаем, прежде чем принимать их синхронно, используя sigwait
(который должен возвращаться немедленно здесь, если только какая-то другая часть программы не создает состояние гонки). Наконец, мы устанавливаем соответствующий флаг.
- Мы должны удалить нашу сигнальную маску в конце (опционально с одной последней проверкой сигналов).
Есть несколько важных соображений производительности и предостережений:
- Предполагая, что каждая итерация внутреннего цикла мала, выполнение системных вызовов проверки сигнала является дорогостоящим. В примере кода мы проверяем наличие сигналов только каждые 10 миллионов (для каждого потока) итераций, что соответствует, возможно, паре секунд времени стены.
omp for
циклы не могут быть разбиты на 1, поэтому вы должны либо прокрутить оставшиеся итерации, либо переписать цикл, используя более простые примитивы OpenMP. Обычные циклы (например, внутренние циклы внешнего параллельного цикла) могут быть просто разбиты.
- Если только главный поток может проверять наличие сигналов, это может создать проблему в программах, где главный поток завершается намного раньше других потоков. В этом сценарии эти другие потоки будут непрерывными. Чтобы решить эту проблему, вы можете "передать эстафетную палочку" проверки сигналов, когда каждый поток завершает свою рабочую нагрузку, или можно заставить главный поток продолжать работу и опрос, пока все остальные потоки не завершат работу2.
- В некоторых архитектурах, таких как высокопроизводительные компьютеры NUMA, время на проверку «глобального» сигнального флага может быть довольно дорогим, поэтому будьте осторожны при принятии решения о том, когда и где проверять или манипулировать флагом. Например, для секции спинового цикла может потребоваться локальное кэширование флага, когда он становится истинным.
Вот пример кода:
#include <signal.h>
void calculate() {
_Bool signalled = false;
int sigcaught;
size_t steps_tot = 0;
// block signals of interest (SIGINT and SIGTERM here)
sigset_t oldmask, newmask, sigpend;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigaddset(&newmask, SIGTERM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
#pragma omp parallel
{
int rank = omp_get_thread_num();
size_t steps = 0;
// keep improving result forever, unless signalled
while (!signalled) {
#pragma omp for
for (size_t i = 0; i < 10000; i++) {
// we can't break from an omp for loop...
// instead, spin away the rest of the iterations
if (signalled) continue;
for (size_t j = 0; j < 1000000; j++, steps++) {
// ***
// heavy computation...
// ***
// check for signal every 10 million steps
if (steps % 10000000 == 0) {
// master thread; poll for signal
if (rank == 0) {
sigpending(&sigpend);
if (sigismember(&sigpend, SIGINT) || sigismember(&sigpend, SIGTERM)) {
if (sigwait(&newmask, &sigcaught) == 0) {
printf("Interrupted by %d...\n", sigcaught);
signalled = true;
}
}
}
// all threads; stop computing
if (signalled) break;
}
}
}
}
#pragma omp atomic
steps_tot += steps;
}
printf("The result is ... after %zu steps\n", steps_tot);
// optional cleanup
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
Если вы используете C++, вам может пригодиться следующий класс...
#include <signal.h>
#include <vector>
class Unterminable {
sigset_t oldmask, newmask;
std::vector<int> signals;
public:
Unterminable(std::vector<int> signals) : signals(signals) {
sigemptyset(&newmask);
for (int signal : signals)
sigaddset(&newmask, signal);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
}
Unterminable() : Unterminable({SIGINT, SIGTERM}) {}
// this can be made more efficient by using sigandset,
// but sigandset is not particularly portable
int poll() {
sigset_t sigpend;
sigpending(&sigpend);
for (int signal : signals) {
if (sigismember(&sigpend, signal)) {
int sigret;
if (sigwait(&newmask, &sigret) == 0)
return sigret;
break;
}
}
return -1;
}
~Unterminable() {
sigprocmask(SIG_SETMASK, &oldmask, NULL);
}
};
Затем блокирующая часть calculate()
может быть заменена на Unterminable unterm();
, а часть проверки сигнала на if ((sigcaught = unterm.poll()) > 0) {...}
. Разблокировка сигналов выполняется автоматически, когда unterm
выходит за рамки.
1 Это не совсем так. OpenMP поддерживает ограниченную поддержку выполнения «параллельного разрыва» в виде отмены точек< /а>. Если вы решите использовать точки отмены в своих параллельных циклах, убедитесь, что вы точно знаете, где находятся неявные точки отмены, чтобы гарантировать согласованность данных вычислений после отмены.
2 Лично я подсчитываю, сколько потоков завершили цикл for, и, если главный поток завершает цикл, не перехватив сигнал, он продолжает опрашивать сигналы до тех пор, пока не поймает сигнал или все потоки завершают цикл. Для этого обязательно пометьте цикл for nowait
.
person
sourtin
schedule
01.05.2019