Опубликован: 06.12.2004 | Доступ: свободный | Студентов: 1180 / 143 | Оценка: 4.76 / 4.29 | Длительность: 20:58:00
ISBN: 978-5-9556-0021-5
Лекция 2:

Средства синхронизации потоков управления

Барьеры

Барьеры – весьма своеобразное средство синхронизации. Идея его в том, чтобы в определенной точке ожидания собралось заданное число потоков управления. Только после этого они смогут продолжить выполнение. (Поговорка "семеро одного не ждут" к барьерам не применима.)

Барьеры полезны для организации коллективных распределенных вычислений в многопроцессорной конфигурации, когда каждый участник ( поток управления ) выполняет часть работы, а в точке сбора частичные результаты объединяются в общий итог.

Функции, ассоциированные с барьерами, подразделяются на следующие группы.

  • инициализация и разрушение барьеров: pthread_barrier_init(), pthread_barrier_destroy() (см. листинг 2.36);
    #include <pthread.h>
    
    int pthread_barrier_init (
      pthread_barrier_t *restrict barrier, 
        const pthread_barrierattr_t
          *restrict attr,
          unsigned count);
    
    int pthread_barrier_destroy (
      pthread_barrier_t *barrier);
    Листинг 2.36. Описание функций инициализации и разрушения барьеров.
  • синхронизация на барьере: pthread_barrier_wait() (см. листинг 2.37);
    #include <pthread.h>
    int pthread_barrier_wait (
      pthread_barrier_t *barrier);
    Листинг 2.37. Описание функции синхронизации на барьере.
  • инициализация и разрушение атрибутных объектов барьеров: pthread_barrierattr_init(), pthread_barrierattr_destroy() (см. листинг 2.38);
    #include <pthread.h>
    
    int pthread_barrierattr_init (
      pthread_barrierattr_t *attr);
    
    int pthread_barrierattr_destroy (
      pthread_barrierattr_t *attr);
    Листинг 2.38. Описание функций инициализации и разрушения атрибутных объектов барьеров.
  • опрос и установка атрибутов барьеров в атрибутных объектах: pthread_barrierattr_getpshared(), pthread_barrierattr_setpshared() (см. листинг 2.39).
    #include <pthread.h>
    
    int pthread_barrierattr_getpshared 
          (const pthread_barrierattr_t
            *restrict attr, 
          int *restrict pshared);
    
    int pthread_barrierattr_setpshared 
          (pthread_barrierattr_t *attr,
            int pshared);
    Листинг 2.39. Описание функций опроса и установки атрибутов барьеров в атрибутных объектах.

Обратим внимание на аргумент count в функции инициализации барьера pthread_barrier_init(). Он задает количество синхронизируемых потоков управления. Столько потоков должны вызвать функцию pthread_barrier_wait(), прежде чем каждый из них сможет успешно завершить вызов и продолжить выполнение. (Разумеется, значение count должно быть положительным.)

Когда к функции pthread_barrier_wait() обратилось требуемое число потоков управления, одному из них (стандарт POSIX-2001 не специфицирует, какому именно) в качестве результата возвращается именованная константа PTHREAD_BARRIER_SERIAL_THREAD, а всем другим выдаются нули. После этого барьер возвращается в начальное (инициализированное) состояние, а выделенный поток может выполнить соответствующие объединительные действия.

Описанная схема работы проиллюстрирована листингом 2.40.

if ((status = pthread_barrier_wait(
  &barrier)) ==
        PTHREAD_BARRIER_SERIAL_THREAD) {
 /* Выделенные (обычно – объединительные) */
 /* действия.     */
 /* Выполняются каким-то одним потоком */
 /* управления */

} else {
    /* Эта часть выполняется всеми */
    /* прочими потоками */
    /* управления */
    if (status != 0) {
    /* Обработка ошибочной ситуации */
    } else {
        /* Нормальное "невыделенное" */
        /* завершение ожидания */
        /* на барьере */
    }
}

/* Повторная синхронизация – */
/* ожидание завершения выделенных действий */
status = pthread_barrier_wait (&barrier);
/* Продолжение параллельной работы */
    . . .
Листинг 2.40. Типичная схема применения функции pthread_barrier_wait().

Отметим, что для барьеров отсутствует вариант синхронизации с контролем времени ожидания. Это вполне понятно, поскольку в случае срабатывания контроля барьер окажется в неработоспособном состоянии (требуемое число потоков, скорее всего, уже не соберется). По той же причине функция pthread_barrier_wait() не является точкой терминирования – "оставшиеся в живых" не переживут потери товарища...

Аналогично, не являются точками терминирования функции pthread_mutex_lock() и pthread_spin_lock(). Если бы они были таковыми, то точками терминирования стали бы все функции, в том числе библиотечные, которые их вызывают – malloc(), free() и т.п. Обеспечить в обработчиках завершения корректное состояние объектов, обслуживаемых подобными функциями, довольно сложно; это деятельность, чреватая ошибками, которые трудно не только найти и исправить, но даже воспроизвести. С другой стороны, мьютексы и спин-блокировки предназначены для захвата на короткое время, без длительного ожидания, так что нечувствительность к терминированию в данном случае не составляет большой проблемы.

Работу с барьерами проиллюстрируем коллективными вычислениями, производимыми двумя потоками (см. листинг 2.41).

/* * * * * * * * * * * * * * * * * * * * * * */
/* Программа использует барьеры для слияния  */
/* результатов    */
/* коллективных вычислений ln(2) */
/* * * * * * * * * * * * * * * * * * * * * * */

#define _XOPEN_SOURCE 600

#include <pthread.h>
#include <stdio.h>
#include <errno.h>

static pthread_barrier_t mbrr;

/* Ряд для ln(2) будут суммировать */
/* два потока.    */
/* Один возьмет на себя положительные */
/* слагаемые,     */
/* другой – отрицательные         */
static double sums [2] = {0, 0};

/* * * * * * * * * * * * * * * * * * * * */
/* Стартовая функция потоков управления, */
/* участвующих в вычислениях ln(2).     */
/* Аргумент ns на самом деле     */
/* целочисленный и равен 1 или 2     */
/* * * * * * * * * * * * * * * * * * * * */
static void *start_func (void *ns) {
    double d = 1;
    double s = 0;
    int i;

    /* Вычислим свою часть ряда */
    for (i = (int) ns; i <= 100000000;
      i += 2) {
        s += d / i;
    }

    /* Запомним результат в нужном месте */
    sums [(int) ns – 1] = s;

    /* Синхронизируемся для получения */
    /* общего итога */
    if (pthread_barrier_wait (&mbrr) == 
      PTHREAD_BARRIER_SERIAL_THREAD) {
        sums [0] -= sums [1];
    }
/* Указатель на итог возвращают оба потока */
    return (sums);
}

/* * * * * * * * * * * * * * * * * */
/* Инициализация барьера,          */
/* создание и ожидание завершения  */
/* потоков управления     */
/* * * * * * * * * * * * * * * * * */
int main (void) {
    pthread_t pt1, pt2;
    double *pd;

    if ((errno = pthread_barrier_init (
      &mbrr, NULL, 2)) != 0) {
        perror ("PTHREAD_BARRIER_INIT");
        return (errno);
    }

    pthread_create (&pt1, NULL,
      start_func, (void *) 1);
    pthread_create (&pt2, NULL,
      start_func, (void *) 2);

    pthread_join (pt1,
      (void **) &pd);
    pthread_join (pt2,
      (void **) &pd);

    printf ("Коллективно вычисленное"
            " значение ln(2): %g\n", *pd);

    return (pthread_barrier_destroy(
      &mbrr));
}
Листинг 2.41. Пример программы, использующей барьеры.

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