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

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

Далее, как и для мьютексов, мы остановимся только на специфических особенностях переменных условия.

Функции pthread_cond_wait() и pthread_cond_timedwait() блокируют вызывающий поток управления на переменной условия cond. Этот поток должен являться владельцем мьютекса, заданного аргументом mutex.

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

По завершении ожидания на переменной условия cond поток управления вновь становится владельцем мьютекса mutex. Это дает ему возможность проверить истинность предиката и либо продолжить выполнение, выйдя из цикла, либо возобновить ожидание.

Если значение предиката оказалось ложным, ложным называется и разблокирование потока управления. Для функции pthread_cond_wait() оно может объясняться по крайней мере двумя причинами:

  • поток управления, изменивший значения разделяемых переменных и разблокировавший ждущих, не обеспечил истинности предиката ;
  • еще один поток, ожидавший на переменной условия cond и получивший управление в первую очередь, нарушил истинность предиката.

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

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

rc = 0;
while (! predicate && rc == 0) {
    rc = pthread_cond_timedwait (
      &cond, &mutex, &ts);
}
Листинг 2.17. Типичный цикл ожидания на переменной условия с контролем по времени.

Если бы аргумент ts задавался как интервал, его пришлось бы перевычислять в цикле; кроме того, контроль по времени оказался бы смазанным непредсказуемыми задержками в выполнении потока управления.

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

Функция pthread_cond_broadcast() разблокирует все потоки управления, ждущие на переменной условия (это полезно, например, когда писатель уступает место читателям ), а функция pthread_cond_signal() (не генерирующая, вопреки названию, никаких сигналов) – по крайней мере один из них (если таковые вообще имеются). Порядок разблокирования определяется политикой планирования; борьба за владение мьютексом, заданным в начале ожидания, протекает так же, как и при одновременном вызове pthread_mutex_lock (mutex).

Вообще говоря, поток управления, вызвавший pthread_cond_broadcast() или pthread_cond_signal(), не обязан быть владельцем упомянутого мьютекса ; тем не менее, если требуется предсказуемость решений планировщика, это условие должно быть выполнено.

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

Не рекомендуется вызывать pthread_cond_signal() (а также освобождать мьютексы ) в (асинхронно выполняемых) функциях обработки сигналов.

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

/* * * * * * * * * * * * * * * * * */
/* Многопотоковый вариант обеда    */
/* философов с использованием      */
/* мьютексов и переменных условия  */
/* * * * * * * * * * * * * * * * * */

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

/* Число обедающих философов */
#define QPH             5

/* Время (в секундах) на обед */
#define FO             15

/* Длительность еды */
#define ernd (rand () % 3 + 1)

/* Длительность разговора */
#define trnd (rand () % 5 + 1)

/* Состояние вилок */
static int fork_busy [QPH] = {0, };

/* Синхронизирующая переменная условия */
static pthread_cond_t forks_cond =
  PTHREAD_COND_INITIALIZER;

/* Синхронизирующий мьютекс */
static pthread_mutex_t forks_mutex =
  PTHREAD_MUTEX_INITIALIZER;

/* * * * * * * * * * * * * * * * * * */
/* Стартовая функция потока-философа.     */
/* Аргумент – номер философа     */
/* * * * * * * * * * * * * * * * * * */
void *start_phil (void *no) {
    /* Время до конца обеда */
    int fo;
    /* Время очередного отрезка */
    /* еды или беседы */     
    int t;         

    fo = FO;
    while (fo > 0) { /* Обед */

        /* Философ говорит */
        printf ("Философ %d беседует\n",
          (int) no);
        t = trnd; sleep (t); fo -= t;

        /* Пытается взять вилки */
        (void) pthread_mutex_lock (
          &forks_mutex);
        while (fork_busy [(int) no – 1]
          || 
          fork_busy [(int) no % QPH] ) {
            (void) pthread_cond_wait
            (
            &forks_cond, 
            &forks_mutex);
        }
        fork_busy [(int) no – 1] = 
            fork_busy [(int) no
              % QPH] = 1;
        (void) pthread_mutex_unlock (
          &forks_mutex);

        /* Ест */
        printf ("Философ %d ест\n",
          (int) no);
        t = ernd; sleep (t); fo -= t;

        /* Отдает вилки */
        (void) pthread_mutex_lock (
          &forks_mutex);
        fork_busy [(int) no – 1] = 
            fork_busy [(int) no 
            % QPH] = 0;
        (void) pthread_cond_broadcast
          (&forks_cond);
        (void) pthread_mutex_unlock
          (&forks_mutex);
    } /* while */

    printf ("Философ %d закончил обед\n",
      (int) no);
    return (NULL);
}

/* * * * * * * * * * * * * * * */
/* Создание потоков-философов  */
/*  и ожидание их завершения   */
/* * * * * * * * * * * * * * * */
int main (void) {
/* Массив идентификаторов */
/* потоков-философов */
    pthread_t pt_id [QPH];
    /* Номер философа */    
    int no;
    /* Атрибутный объект для */
    /* создания потоков */                 
    pthread_attr_t attr_obj; 

    if ((errno = pthread_attr_init(
      &attr_obj)) != 0) {
        perror ("PTHREAD_ATTR_INIT");
        return (errno);
    }

    /* В очередь, господа */
    /* философы, в очередь! */
    if ((errno = 
            pthread_attr_setschedpolicy(
            &attr_obj, 
            SCHED_FIFO)) != 0) {
        perror (
          "PTHREAD_ATTR_SETSCHEDPOLICY");
        return (errno);
    }

    /* Все – к столу */
    for (no = 1;  no <= QPH; no++) {
        if ((errno = pthread_create (
           &pt_id [no – 1], 
                &attr_obj,
                start_phil,
                (void *) no))
                 != 0)
            {
            perror (
              "PTHREAD_CREATE");
            return (no);
        }
    }

    /* Ожидание завершения обеда */
    for (no = 1; no <= QPH; no++) {
        (void) pthread_join (
         pt_id [no – 1], NULL);
    }

    return 0;
}
Листинг 2.18. Пример многопотоковой реализации обеда философов с использованием мьютексов и переменных условия.

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

Философ 1 беседует
Философ 2 беседует
Философ 3 беседует
Философ 4 беседует
Философ 5 беседует
Философ 4 ест
Философ 2 ест
Философ 4 беседует
Философ 5 ест
Философ 2 беседует
Философ 3 ест
Философ 5 беседует
Философ 1 ест
Философ 3 беседует
Философ 4 ест
Философ 1 беседует
Философ 2 ест
Философ 2 беседует
Философ 1 ест
Философ 4 беседует
Философ 3 ест
Философ 1 беседует
Философ 5 ест
Философ 5 беседует
Философ 3 беседует
Философ 4 ест
Философ 2 ест
Философ 4 беседует
Философ 2 беседует
Философ 1 ест
Философ 3 ест
Философ 1 закончил обед
Философ 5 ест
Философ 3 закончил обед
Философ 2 ест
Философ 5 закончил обед
Философ 4 ест
Философ 2 закончил обед
Философ 4 закончил обед
Листинг 2.19. Возможные результаты работы многопотоковой программы, моделирующей обед философов с использованием мьютексов и переменных условия.

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

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

Обратим внимание на то, что цикл, в который заключено ожидание на переменной условия, в данном случае использован для захвата нескольких ресурсов. Это напоминает описанные в курсе [1] групповые операции с семафорами, если иметь в виду цикл в целом и отвлечься от ложных разблокирований.

Разумеется, приведенное решение задачи об обедающих философах является нечестным, поскольку во время ожидания на переменной условия, которое длится неопределенно долго, философ "отключается", он не беседует и не ест, что, по условию задачи, запрещено (этим недостатком страдает и программа из курса [1], основанная на групповых операциях с семафорами ). Однако как иллюстрация типичных способов работы с мьютексами и переменными условия данная программа имеет право на существование.