Опубликован: 28.04.2010 | Уровень: специалист | Доступ: платный | ВУЗ: Новосибирский Государственный Университет
Лекция 7:

Блокировки чтения-записи, условные переменные, барьеры и семафоры-счетчики

< Лекция 6 || Лекция 7: 12 || Лекция 8 >
Аннотация: В ходе этой лекции вы изучите использование следующих примитивов взаимоисключения и синхронизации: блокировок чтения-записи, условных переменных, барьеров, семафоров-счетчиков , блокировки чтения-записи.

Не надо звонить, мы поймаем машину внизу

Надеюсь, что ты разбудишь меня

Не раньше, чем нас довезут

Б. Гребенщиков

Блокировки чтения-записи (read-write locks) похожи на мутексы, но отличаются от них тем, что имеют два режима захвата - для чтения и для записи. Блокировку для чтения могут удерживать несколько нитей одновременно. Блокировку для записи может удерживать только одна нить; при этом никакая другая нить не может удерживать эту же блокировку для чтения. API для работы с блокировками чтения-записи в целом похож на API для работы с мутексами и включает в себя следующие функции:

  • pthread_rwlock_init(3C)
  • pthread_rwlock_rdlock(3C)
  • pthread_rwlock_tryrdlock(3C)
  • pthread_rwlock_timedrdlock(3C)
  • pthread_rwlock_wrlock(3C)
  • pthread_rwlock_trywrlock(3C)
  • pthread_rwlock_timedwrlock(3C)
  • pthread_rwlock_unlock(3C)
  • pthread_rwlock_destroy(3C)
  • pthread_rwlockattr_init
  • pthread_rwlockattr_destroy
  • pthread_rwlockattr_getpshared(3C)
  • pthread_rwlockattr_setpshared(3C)

Набор атрибутов pthread_rwlockattr_t существенно беднее, чем у мутекса, и включает только атрибут pshared, управляющий областью действия блокировки. Применение блокировок чтения-записи достаточно очевидно. Они используются для защиты структур данных, которые читают значительно чаще, чем модифицируют.

Условные переменные

Условная переменная представляет собой примитив синхронизации. Над ней определены две основные операции: wait и signal. Нить, выполнившая операцию wait, блокируется до того момента, пока другая нить не выполнит операцию signal. Таким образом, операцией wait первая нить сообщает системе, что она ждет выполнения какого-то условия, а операцией signal вторая нить сообщает первой, что параметры, от которых зависит выполнение условия, возможно, изменились.

Основное применение условных переменных - это сценарий производитель-потребитель. Рассмотрим две нити, одна из которых генерирует данные,а другая - перерабатывает их. В простейшем случае производитель помещает каждую следующую порцию данных в разделяемую переменную, а потребитель считывает ее оттуда. При этом могут возникать две проблемы. Если производитель работает быстрее потребителя, то он может записать очередную порцию данных до того, как потребитель прочитает предыдущую. При этом предыдущая порция данных будет потеряна. Если же потребитель работает быстрее производителя, он может обработать одну и ту же порцию данных несколько раз. Легко понять, что при помощи одного мутекса эти проблемы устранить невозможно. Несколько сложнее понять, что их нельзя решить и при помощи двух мутексов (в качестве упражнения слушателю предлагается доказать это утверждение). Даже при использовании трех мутексов мы будем вынуждены использовать для начальной синхронизации холостой цикл или какой-то другой примитив межпоточного взаимодействия.

Классическое решение этой задачи реализуется с использованием условной переменной (см. пример 7.1).

int full; 
pthread_mutex_t mx; 
pthread_cond_t cond; 
int data; 
void *producer(void *) { 
  while(1) { 
    int t=produce(); 
    pthread_mutex_lock(&mx) 
    while(full) { 
      pthread_cond_wait(&cond, &mx); 
    } 
    data=t; 
    full=1; 
    pthread_cond_signal(&cond); 
    pthread_mutex_unlock(&mx); 
  } 
  return NULL; 
} 

void * consumer(void *) { 
  while (1) { 
    int t; 
    pthread_mutex_lock(&mx); 
    while (!full) { 
      pthread_cond_wait(&cond, &mx); 
    } 
    t=data; 
    full=0; 
    pthread_cond_signal(&mx); 
    pthread_mutex_unlock(&mx); 
    consume(1); 
  } 
  return NULL; 
}
7.1. Решение задачи "производитель-потребитель" (фрагмент)

Условную переменную можно также использовать для реализации группового (атомарного) захвата нескольких мутексов (см. пример 7.2)

void get_forks (int phil, int fork1, int fork2) { 
  int res; 
  pthread_mutex_lock(&getting_forks_mx); 
  do {
    if (res=pthread_mutex_trylock(&forks[fork1])) { 
      res=pthread_mutex_trylock(&forks[fork2]); 
      if (res) pthread_mutex_unlock(&forks[fork1]);
    }
    if (res) pthread_cond_wait(&getting_forks_cond, &getting_forks_mx); 
  } while(res); 
  pthread_mutex_unlock(&getting_forks_mx); 
} 

void down_forks (int f1, int f2) { 
  pthread_mutex_lock(&getting_forks_mx); 
  pthread_mutex_unlock (&forks[f1]); 
  pthread_mutex_unlock (&forks[f2]); 
  pthread_cond_broadcast(&getting_forks_cond); 
  pthread_mutex_unlock(&getting_forks_mx); 
}
7.2. Реализация атомарного захвата нескольких мутексов

Точное описание действия функции pthread_cond_wait(3C) звучит так. Эта функция имеет два параметра, pthread_cond_t * cond и pthread_mutex_t *mx. При вызове wait мутекс должен быть захвачен, в противном случае результат не определен. Wait освобождает мутекс и блокирует нить до момента вызова другой нитью pthread_cond_signal. После пробуждения wait пытается захватить мутекс; если это не получается, он блокируется до того момента, пока мутекс не освободят.

Мутекс используется для защиты данных, используемых при вычислении условия, с которым связана наша условная переменная. Условие необходимо проверять как перед вызовом pthread_cond_wait(3C), так и после выхода из этой функции. Проверка условия перед вызовом позволяет защититься от так называемой "ошибки потерянного пробуждения" (lost wakeup), т.е. от ситуации, когда производитель вызвал signal в то время, когда потребитель еще не был заблокирован в wait.

Интересный вопрос, обсуждающийся во многих документах - это следует ли вызывать pthread_cond_signal(3C), удерживая мутекс mx, или нет. Сама по себе функция pthread_cond_signal(3C) нормально работает даже если нить не удерживает мутекс. Однако удержание мутекса mx (того же мутекса, который используется в параметрах pthread_cond_wait(3C) ) гарантирует нас от посылки сигнала в момент проверки ждущей нитью условия или в интервале между проверкой условия и входом в wait. Таким образом, сигнализирование при свободном мутексе может привести к ошибке потерянного пробуждения.

Необходимость в проверке условия после выхода из wait не столь очевидна.

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

Полный список операций, определенных над условной переменной, таков:

  • pthread_condattr_destroy(3C) - уничтожение атрибутов условной переменной
  • pthread_condattr_getpshared(3C) - получение значения атрибута pshared
  • pthread_condattr_init(3C) - инициализация атрибутов условной переменной
  • pthread_condattr_setpshared(3C) - установка значения атрибута pshared
  • pthread_cond_broadcast(3C) - широковещательный вариант операции signal
  • pthread_cond_destroy(3C) - уничтожение условной переменной
  • pthread_cond_init(3C) - инициализация условной переменной
  • pthread_cond_reltimedwait_np(3C) - ожидание с тайм-аутом
  • pthread_cond_signal(3C) - операция signal
  • pthread_cond_timedwait(3C) - ожидание с тайм-аутом
  • pthread_cond_wait(3C) - ожидание условной переменной (операция wait )

Процедура создания и уничтоженияусловных переменных в целом аналогична процедуре создания и уничтожения ранее рассматривавшихся объектов синхронизации.

< Лекция 6 || Лекция 7: 12 || Лекция 8 >
Dima Puvovarov
Dima Puvovarov
Россия
Святослав Песенко
Святослав Песенко
Украина, Кривой Рог, КГПУ, 2006