Использование переменных условия
Переменные условия позволяют приостановить выполнение нити, пока не наступит заданное событие или не будет соблюдено некоторое условие.
- Булевская переменная, которая указывает, выполнено ли условие
- Взаимная блокировка, которая упорядочивает доступ к булевской переменной
- Переменная условия, которая ожидает соблюдения условия
Работать с переменными условия непросто, но зато они позволяют реализовать мощные и эффективные механизмы синхронизации. Дополнительная информация о реализации длительных блокировок и семафоров с помощью переменных условия приведена в разделе Создание сложных объектов синхронизации.
После завершения работы нити соответствующая память может не очищаться; это зависит от атрибутов нити. Такие нити можно соединять с другими нитями для получения доступа к оставшейся информации. Нить, к которой нужно подключить другую нить, блокируется до завершения работы подключенной нити. Механизм подключения представляет собой разновидность механизма переменных условия, причем условием является завершение работы нити.
Объект условных атрибутов
Так же как нити и взаимные блокировки, переменные условия создают с помощью объекта атрибутов. Объект атрибутов условия - это абстрактный объект, который содержит один атрибут или не содержит ни одного, в зависимости от варианта реализации опций POSIX. It is accessed through a variable of type pthread_condattr_t. В AIX тип данных pthread_condattr_t обозначает указатель; в других системах это может быть структура или другой тип данных.
Создание и удаление объектов атрибутов условия
Объект атрибутов условия инициализируется значениями по умолчанию с помощью функции pthread_condattr_init. Ряд функций предусмотрен и для работы с атрибутами. За удаление объекта атрибутов нити отвечает функция pthread_condattr_destroy. В зависимости от способа реализации библиотеки нитей, эта функция может освобождать память, динамически выделенную функцией pthread_condattr_init.
pthread_condattr_t attributes;
/* создается объект атрибутов */
...
if (!pthread_condattr_init(&attributes)) {
/* объект атрибутов инициализируется */
...
/* работа с объектом атрибутов */
...
pthread_condattr_destroy(&attributes);
/* удаление объекта атрибутов */
}
Один и тот же объект атрибутов можно использовать для создания нескольких переменных условия. Кроме того, его можно изменять в период между созданием двух переменных условия. После создания переменных условия соответствующий объект атрибутов можно удалить - это не повлияет на переменные.
Атрибут условия
Поддерживаются следующие атрибуты условия:
- Совместного выполнения процессов
- Задает параметры совместного использования процесса переменной условия. Данный атрибут зависит от опций POSIX совместного использования процесса.
Создание и удаление переменных условия
pthread_cond_t cond;
pthread_condattr_t attr;
...
pthread_condattr_init(&attr);
pthread_cond_init(&cond, &attr);
pthread_condattr_destroy(&attr);
pthread_cond_t cond;
...
pthread_cond_init(&cond, NULL);
ИД созданной переменной возвращается в вызывающую нить через параметр condition. ИД условия - это объект со скрытой реализацией типа pthread_cond_t. В AIX тип данных pthread_cond_t является структурой; в других системах он может быть указателем или каким-либо другим типом данных.
- Вызывайте функцию pthread_cond_init до создания других нитей, которые будут использовать эту переменную, например, в главной нити.
- Вызывайте функцию pthread_cond_init из программы с однократной инициализацией. Дополнительная информация приведена в разделе Разовая инициализация.
- Используйте статическую переменную условия, которая инициализируется специальной макрокомандой PTHREAD_COND_INITIALIZER; в этом случае переменная условия инициализируется со стандартными атрибутами по умолчанию.
После того как переменная условия станет не нужна, ее следует удалить с помощью функции pthread_cond_destroy. Это команда восстанавливает всю память, захваченную функцией pthread_cond_init. После удаления переменной условия можно создать другое условие с помощью той же самой переменной pthread_cond_t. Например, приведенный ниже фрагмент кода, хотя и несколько искусственный, не содержит ошибок:
pthread_cond_t cond;
...
for (i = 0; i < 10; i++) {
/* создание переменной условия */
pthread_cond_init(&cond, NULL);
/* применение переменной условия */
/* удаление переменной условия */
pthread_cond_destroy(&cond);
}
Так же как и любой системный ресурс, который может использоваться несколькими нитями, переменная условия должна быть удалена из стека памяти, выделенного нити, до момента завершения нити. Библиотека нитей хранит список переменных условия; таким образом, освобождение стека, в котором находится взаимная блокировка, приведет к повреждению списка.
Использование переменных условия
struct condition_bundle_t {
int condition_predicate;
pthread_mutex_t condition_lock;
pthread_cond_t condition_variable;
};
Ожидание выполнения условия
Взаимная блокировка, соответствующая данному условию, до начала ожидания выполнения условия должна быть деактивизирована (заблокирована). Для перевода нити в состояние ожидания события предназначены функции pthread_cond_wait и pthread_cond_timedwait. Функция автоматически активизирует взаимную блокировку и блокирует вызывающую нить до появления сигнала о выполнения условия. После возврата запроса взаимная блокировка вновь деактивизируется.
Функция pthread_cond_wait блокирует нить на неопределенное время. Если условие так никогда и не будет выполнено, то нить никогда не возобновится. Поскольку в функции pthread_cond_wait предусмотрена точка отмены нити, единственный способ выхода из этой тупиковой ситуации - отменить блокированную нить, если такая отмена разрешена. Дополнительная информация приведена в разделе Принудительное завершение работы нити.
Функция pthread_cond_timedwait блокирует нить только на заданный промежуток времени. У нее есть дополнительный параметр timeout, который указывает дату и время снятия блокировки. Параметр timeout - это указатель на структуру timespec. Такой тип данных также называется timestruc_t. Он содержит следующие поля:
- tv_sec
- Длинное целое без знака, задающее количество секунд
- tv_nsec
- Длинное целое, задающее количество наносекунд
struct timespec timeout;
...
time(&timeout.tv_sec);
timeout.tv_sec += МАКС_ПРОДОЛЖИТЕЛЬНОСТЬ_ОЖИДАНИЯ;
pthread_cond_timedwait(&cond, &mutex, &timeout);
Параметр timeout задает абсолютное время снятия блокировки. Приведенный выше код показывает, каким образом можно задать не абсолютную дату, а продолжительность ожидания.
struct tm date;
time_t seconds;
struct timespec timeout;
...
date.tm_sec = 0;
date.tm_min = 0;
date.tm_hour = 8;
date.tm_mday = 1;
date.tm_mon = 0; /* диапазон возможных значений: 0-11 */
date.tm_year = 101; /* 0 означает 1900 */
date.tm_wday = 1; /* это необязательное поле, но
в действительности это будет понедельник! */
date.tm_yday = 0; /* первый день года */
date.tm_isdst = daylight;
/* сезонное время - это внешняя переменная. Предполагается,
что сезонное время по-прежнему будет применяться... */
seconds = mktime(&date);
timeout.tv_sec = (длинное без знака)seconds;
timeout.tv_nsec = 0L;
pthread_cond_timedwait(&cond, &mutex, &timeout);
Несмотря на то, что ожидание в функции pthread_cond_timedwait не является бесконечным, в ней предусмотрена точка отмены операции. Таким образом, ожидающую нить можно отменить, не дожидаясь тайм-аута.
Передача сигнала
Сигнал может быть передан функцией pthread_cond_signal или pthread_cond_broadcast.
Функция pthread_cond_signal активизирует по крайней мере одну нить, которая в данное время ожидает выполнения определенного условия. Нить для активизации выбирается в соответствии со стратегией планирования; а именно, выбирается нить с наивысшим приоритетом планирования (см. раздел Стратегия и приоритеты планирования). Учтите, что в многопроцессорных системах и некоторых операционных системах, отличных от AIX, может быть активизировано несколько нитей. Не следует рассчитывать на то, что эта функция активизирует ровно одну нить.
Функция pthread_cond_broadcast активизирует все нити, которые ожидают выполнения заданного условия. Однако после завершения функции нить можно вновь заблокировать до тех пор, пока не будет выполнено то же самое условие.
Если параметр cond задан верно, то описанные функции всегда завершают работу успешно. Однако это не означает, что какая-либо нить обязательно будет активизирована. Более того, библиотека не сохраняет информации о сигнале и о выполнении условия. Например, рассмотрим условие C. Его выполнения не ожидает ни одна нить. В момент t нить 1 сигнализирует о выполнении условия C. Вызов успешно выполняется, хотя ни одна нить не активизируется. В момент времени t+1 нить 2 вызывает функцию pthread_cond_wait со значением параметра cond, равным C. При этом нить 2 блокируется. Если ни одна из других нитей не подаст сигнала о выполнении условия C, то вполне возможно, что нить 2 будет оставаться заблокированной вплоть до завершения процесса.
Для того чтобы избежать этой тупиковой ситуации, можно проверить код ошибки EBUSY, который функция pthread_cond_destroy возвращает при удалении переменной условия. Это показано в следующем фрагменте кода:
Функция pthread_yield позволяет запланировать другую нить, например, одну из активизированных нитей. Дополнительная информация о функции pthread_yield .
Функции pthread_cond_wait и pthread_cond_broadcast нельзя применять вместе с обработчиком сигнала. Для организации ожидания сигнала в библиотеке нитей предусмотрена функция sigwait. Дополнительная информация о функции sigwait. За дополнительной информацией о функции sigwait обратитесь к разделу Управление сигналами.
Синхронизация нитей с помощью условных переменных
while (pthread_cond_destroy(&cond) == EBUSY) {
pthread_cond_broadcast(&cond);
pthread_yield();
}
Переменные условия применяются
для организации ожидания выполнения определенного предиката
условия. Этот предикат устанавливает другая нить, обычно та,
которая сигнализирует о выполнении условия.Семантика условия
Предикат условия должен быть защищен взаимной блокировкой. Функция ожидания ( pthread_cond_wait или pthread_cond_timedwait) автоматически активизирует взаимную блокировку и блокирует нить на время ожидания. Когда поступает сигнал о выполнении условия, взаимная блокировка вновь деактивизируется и функция ожидания возвращает управление. Обратите внимание на то, что успешный возврат управления функцией вовсе не означает, что предикат истинен.
Причина этого заключается в том, что могло быть активизировано несколько нитей: либо нить вызвала функцию pthread_cond_broadcast, либо в результате конкуренции между двумя процессорами две нити были активизированы одновременно. Первая нить, блокирующая взаимную блокировку, будет блокировать и все остальные активизированные нити, участвовавшие в ожидании, до тех пор, пока взаимная блокировка не будет разблокирована программой. Таким образом, к моменту, когда вторая нить получит взаимную блокировку и завершит функцию ожидания, предикат может измениться.
В общем случае каждый раз при завершении ожидания нить должна еще раз проверить предикат и определить, следует ли продолжить выполнение, возобновить ожидание или объявить тайм-аут. Помните, что само по себе завершение функции ожидания не означает, что предикат истинен (ложен).
pthread_mutex_lock(&condition_lock);
while (condition_predicate == 0)
pthread_cond_wait(&condition_variable, &condition_lock);
...
pthread_mutex_unlock(&condition_lock);
Семантика тайм-аута
Когда функция pthread_cond_timedwait завершается из-за наступления тайм-аута, предикат может быть истинным, поскольку время истечения тайм-аута может совпасть с изменением состояния предиката.
int result = CONTINUE_LOOP;
pthread_mutex_lock(&condition_lock);
while (result == CONTINUE_LOOP) {
switch (pthread_cond_timedwait(&condition_variable,
&condition_lock, &timeout)) {
case 0:
if (condition_predicate)
result = PROCEED;
break;
case ETIMEDOUT:
result = condition_predicate ? PROCEED : TIMEOUT;
break;
default:
result = ERROR;
break;
}
}
...
pthread_mutex_unlock(&condition_lock);
Выбрать действие можно с помощью переменной result. Рекомендуется, чтобы операторы, предшествующие освобождению взаимной блокировки, выполнялись как можно быстрее, так как взаимную блокировку нельзя удерживать в течение длительного времени.
Указав в параметре timeout абсолютную дату, вы можете легко установить в программе режим реального времени. В этом случае тайм-аут не нужно пересчитывать каждый раз, когда он используется в цикле (например, в том, который содержит функцию ожидания условия). В тех случаях, когда системные часы периодически переводятся оператором, запланированное ожидание (до абсолютной даты) будет закончено, как только показания системных часов превысят значение параметра timeout.
Пример применения переменных условия
Ниже приведен пример исходного кода процедуры точки синхронизации. Точкой синхронизации называется точка программы, в которой выполнение нитей приостанавливается до тех пор, пока все (или по крайней мере некоторые определенные) нити не достигнут этой точки.
#define SYNC_MAX_COUNT 10
void SynchronizationPoint()
{
/* для гарантированной инициализации применяются статические переменные */
static mutex_t sync_lock = PTHREAD_MUTEX_INITIALIZER;
static cond_t sync_cond = PTHREAD_COND_INITIALIZER;
static int sync_count = 0;
/* блокировка доступа к счетчику */
pthread_mutex_lock(&sync_lock);
/* приращение значения счетчика */
sync_count++;
/* проверка: следует ли продолжать ожидание */
if (sync_count < SYNC_MAX_COUNT)
/* ожидать остальных */
pthread_cond_wait(&sync_cond, &sync_lock);
else
/* оповестить о достижении данной точки всеми нитями */
pthread_cond_broadcast(&sync_cond);
/* активизация взаимной блокировки - в противном случае
из процедуры сможет выйти только одна нить! */
pthread_mutex_unlock(&sync_lock);
}
У такой процедуры есть некоторые недостатки: ее можно применить только один раз и число вызывающих ее нитей обозначено символьной константой. Тем не менее, этот пример отражает основной способ применения переменных условия. Более сложные примеры применения. Более сложные примеры применения переменных условия приведены в разделе Создание сложных объектов синхронизации.