Книга: 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.
- 26.1. Введение
- 26.2. Основные функции для работы с потоками: создание и завершение потоков
- 26.3. Использование потоков в функции str_cli
- 26.4. Использование потоков в эхо-сервере TCP
- 26.5. Собственные данные потоков
- 26.6. Веб-клиент и одновременное соединение (продолжение)
- 26.7. Взаимные исключения
- 26.8. Условные переменные
- 26.9. Веб-клиент и одновременный доступ
- 26.10. Резюме
- Упражнения
- 13.2.4. Условные переменные
- Условные выражения
- Условные переменные
- Условные операторы
- ГЛАВА 7 Взаимные исключения и условные переменные
- Условные блоки
- 4.4.6. Сигнальные (условные) переменные
- 7.5. Условные переменные: ожидание и сигнализация
- 7.6. Условные переменные: время ожидания и широковещательная передача