Обработка сигналов в параллельной программе OpenMP

У меня есть программа, использующая таймер POSIX (timer_create()). По сути, программа устанавливает таймер и начинает выполнять длительные (потенциально бесконечные) вычисления. Когда таймер истекает и вызывается обработчик сигнала, обработчик выводит наилучший результат, который был вычислен, и завершает работу программы.

Я рассматриваю возможность параллельного выполнения вычислений с использованием OpenMP, потому что это должно ускорить его.

В pthreads есть специальные функции, например, для установки масок сигналов для моих потоков или около того. Обеспечивает ли OpenMP такой контроль, или я должен принять тот факт, что сигнал может быть доставлен в любой из потоков, созданных OpenMP?

Кроме того, если я сейчас нахожусь в параллельном разделе своего кода и вызывается мой обработчик, может ли он по-прежнему безопасно убивать приложение (exit(0);) и выполнять такие действия, как блокировка блокировок OpenMP?


person user7610    schedule 15.11.2011    source источник
comment
Может быть, это можно решить, используя один блок, который ловит выход?   -  person Bort    schedule 16.11.2011
comment
Спецификация OpenMP не содержит слово сигнал.   -  person jfs    schedule 16.11.2011


Ответы (2)


Стандарт OpenMP 3.1 ничего не говорит о сигналах.

Насколько я знаю, каждая популярная реализация OpenMP в Linux/UNIX основана на pthreads, поэтому поток OpenMP является потоком pthread. И применяются общие правила pthreads и сигналов.

Предоставляет ли OpenMP такой контроль

Нет какого-либо специального контроля; но вы можете попробовать использовать управление pthread. Единственная проблема заключается в том, чтобы узнать, сколько потоков OpenMP используется и где разместить управляющий оператор.

сигнал может быть доставлен в любой из потоков, создаваемых OpenMP?

По умолчанию да, он будет доставлен в любой поток.

мой обработчик называется,

Применяются обычные правила обработчика сигналов. Функции, разрешенные в обработчике сигналов, перечислены на странице http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_04.html (в конце страницы)

А printf нельзя (write можно). Вы можете использовать printf, если знаете, что в момент сигнала printf не используется ни одним потоком (например, у вас нет printf в параллельной области).

может ли он безопасно убить приложение (exit(0);)

Да, может: abort() и _exit() разрешены из обработчика.

Linux/Unix завершит все потоки, когда любой поток выполнит exit или abort.

и делать такие вещи, как блокировка замков OpenMP?

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

!! ОБНОВИТЬ

Существует пример адаптации сигнализации к OpenMP http://www.cs.colostate.edu/%7Ecs675/OpenMPvsThreads.pdf (OpenMP и многопоточность в C/C++). Вкратце: установить флаг в обработчике и добавить проверку этого флага в каждом потоке на каждой N-й итерации цикла.

Адаптация механизма исключений на основе сигналов к параллельному региону

С приложениями на C/C++ чаще, чем с приложениями на Fortran, происходит то, что программа использует сложный пользовательский интерфейс. Genehunter — это простой пример, когда пользователь может прервать вычисление одного генеалогического дерева, нажав Ctrl-C, чтобы перейти к следующему генеалогическому дереву в клинической базе данных о заболевании. Преждевременное завершение обрабатывается в последовательной версии с помощью механизма исключений, подобного C++, включающего обработчик сигналов, setjump и longjump. OpenMP не позволяет неструктурированному потоку управления пересекать границу параллельной конструкции. Мы изменили обработку исключений в версии OpenMP, превратив обработчик прерываний в механизм опроса. Поток, который перехватывает сигнал control-C, устанавливает общий флаг. Все потоки проверяют флаг в начале цикла, вызывая функцию has_hit_interrupt(), и пропускают итерацию, если она установлена. Когда цикл завершается, мастер проверяет флаг и может легко выполнить длинный прыжок, чтобы завершить исключительный выход (см. рис. 1).

person osgx    schedule 16.11.2011

Это немного поздно, но, надеюсь, этот пример кода поможет другим в аналогичной ситуации!


Как упоминалось в 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
comment
Стоит отметить, что OpenMP может поддерживать потоки между параллельными областями, поэтому сигналы должны быть заблокированы до первого. Что заставляет меня задаться вопросом, есть ли гарантия того, что OpenMP не будет предварительно создавать потоки до того, как встретится с первой параллельной областью и до того, как сигналы будут заблокированы? Это предотвратило бы наследование правильной сигмаски. - person user1768761; 08.04.2020