Книга: UNIX: разработка сетевых приложений

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

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

Взаимное исключение позволяет предотвратить одновременный доступ к совместно используемой (разделяемой) переменной, но для того чтобы перевести поток в состояние ожидания (спящее состояние) до момента выполнения некоторого условия, необходим другой механизм. Продемонстрируем сказанное на следующем примере. Вернемся к нашему веб-клиенту из раздела 26.6 и заменим функцию Solaris thr_join на pthread_join. Но мы не можем вызвать функцию pthread_join до тех пор, пока не будем знать, что выполнение потока завершилось. Сначала мы объявляем глобальную переменную, которая служит счетчиком количества завершившихся потоков, и организуем управление доступом к ней с помощью взаимного исключения.

int ndone; /* количество потоков, завершивших выполнение */
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;

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

void* do_get_read(void *vptr) {
 ...
 Pthread_mutex_lock(&ndone_mutex);
 ndone++;
 Pthread_mutex_unlock(&ndone_mutex);
 return(fptr); /* завершение выполнения потока */
}

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

while (nlefttoread > 0) {
 while (nconn < maxnconn && nlefttoconn > 0) {
  /* находим файл для чтения */
  ...
 }
 /* Проверяем, не завершен ли поток */
 Pthread_mutex_lock(&ndone_mutex);
 if (ndone > 0) {
  for (i =0; i < nfiles; i++) {
   if (file[i].f_flags & F_DONE) {
    Pthread_join(file[i].f_tid, (void**)&fptr);
    /* обновляем file[i] для завершенного потока */
    ...
   }
  }
 }
 Pthread_mutex_unlock(&ndone_mutex);
}

Это означает, что главный поток никогда не переходит в спящее состояние, а просто входит в цикл, проверяя каждый раз значение переменной ndone. Этот процесс называется опросом (polling) и рассматривается как пустая трата времени центрального процессора.

Нам нужен метод, с помощью которого главный цикл мог бы входить в состояние ожидания, пока один из потоков не оповестит его о том, что какая-либо задача выполнена. Эта возможность обеспечивается использованием условной переменной (conditional variable) вместе со взаимным исключением. Взаимное исключение используется для реализации блокирования, а условная переменная обеспечивает сигнальный механизм.

В терминах Pthreads условная переменная — это переменная типа pthread_cond_t. Такие переменные используются в следующих двух функциях:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
Обе функции возвращают: 0 в случае успешного выполнения, положительное значение Exxx в случае ошибки

Слово signal в названии второй функции не имеет отношения к сигналам Unix SIGxxx.

Проще всего объяснить действие этих функций на примере. Вернемся к нашему примеру веб-клиента. Счетчик ndone теперь ассоциируется и с условной переменной, и с взаимным исключением:

int ndone;
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;

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

Pthread_mutex_lock(&ndone_mutex);
ndone++;
Pthread_cond_signal(&ndone_cond);
Pthread_mutex_unlock(&ndone_mutex);

Затем основной цикл блокируется в вызове функции pthread_cond_wait, ожидая оповещения о завершении выполнения потока:

while (nlefttoread > 0) {
 while (nconn < maxnconn && nlefttoconn > 0) {
  /* находим файл для чтения */
  ...
 }
 /* Ждем завершения выполнения какого-либо потока */
 Pthread_mutex_lock(&ndone_mutex);
 while (ndone == 0)
  Pthread_cond_wait(&ndone_cond, &ndone_mutex);
 for (i = 0; i < nfiles; i++) {
  if (file[i].f_flags & F_DONE) {
   Pthread_join(file[i].f_tid, (void**)&fptr);
   /* обновляем file[i] для завершенного потока */
   ...
  }
 }
 Pthread_mutex_unlock(&ndone_mutex);
}

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

Почему взаимное исключение всегда связано с условной переменной? «Условие» обычно представляет собой значение некоторой переменной, используемой совместно несколькими потоками. Взаимное исключение требуется для того, чтобы различные потоки могли задавать и проверять значение условной переменной. Например, если в примере кода, приведенном ранее, отсутствовало бы взаимное исключение, то проверка в главном цикле выглядела бы следующим образом:

/* Ждем завершения выполнения одного или нескольких потоков */
while (ndone == 0)
 Pthread_cond_wait(&ndone_cond, &ndone_mutex);

Но при этом существует вероятность, что последний поток увеличивает значение переменной ndone после проверки главным потоком условия ndone == 0, но перед вызовом функции pthread_cond_wait. Если это происходит, то последний «сигнал» теряется, и основной цикл оказывается заблокированным навсегда, так как он будет ждать события, которое никогда не произойдет.

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

/* Ждем завершения выполнения одного или нескольких потоков */
Pthread_mutex_lock(&ndone_mutex);
while (ndone == 0) {
 Pthread_mutex_unlock(&ndone_mutex);
 Pthread_cond_wait(&ndone_cond, &ndone_mutex);
 Pthread_mutex_lock(&ndone_mutex);
}

Существует вероятность того, что по завершении выполнения поток увеличит на единицу значение переменной ndone и это произойдет между вызовом функций pthread_mutex_unlock и pthread_cond_wait.

Обычно функция pthread_cond_signal выводит из состояния ожидания один поток, на который указывает условная переменная. Существуют ситуации, когда некоторый поток знает, что из состояния ожидания должны быть выведены несколько потоков. В таком случае используется функция pthread_cond_broadcast, выводящая из состояния ожидания все потоки, которые блокированы условной переменной.

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr,
 const struct timespec *abstime);
Обе функции возвращают: 0 в случае успешного выполнения, положительное значение Exxx в случае ошибки

Функция pthread_cond_timedwait позволяет потоку задать предельное время блокирования. Аргумент abstime представляет собой структуру timespec (определенную в разделе 6.9 при рассмотрении функции pselect), которая задает системное время для момента, когда функция должна возвратить управление, даже если к этому моменту условная переменная не подала сигнал. Если возникает такая ситуация, возвращается ошибка ETIME.

В данном случае значение времени является абсолютным значением времени, в отличие от относительного значения разницы во времени (time delta) между некоторыми событиями. Иными словами, abstime — это системное время, то есть количество секунд и наносекунд, прошедших с 1 января 1970 года (UTC) до того момента, когда эта функция должна вернуть управление. Здесь имеется различие как с функцией pselect, так и с функцией select, задающими количество секунд (и наносекунд в случае pselect) до некоторого момента в будущем, когда функция должна вернуть управление. Обычно для этого вызывается функция gettimeofday, которая выдает текущее время (в виде структуры timeval), а затем оно копируется в структуру timespec и к нему добавляется требуемое значение:

struct timeval tv;
struct timespec ts;
if (gettimeofday(&tv, NULL) < 0)
 err_sys("gettimeofday error");
ts.tv_sec = tv.tv_sec + 5; /* 5 с в будущем */
ts.tv_nsec = tv.tv_usec * 1000; /* микросекунды переводим в наносекунды */
pthread_cond_timedwait( , &ts);

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

ПРИМЕЧАНИЕ

В POSIX определена новая функция clock_gettime, возвращающая текущее время в виде структуры timespec.

Оглавление книги


Генерация: 0.090. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз