[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]

Программирование: Разработка и отладка программ


Использование условных переменных

Переменные условия позволяют приостановить выполнение нити, пока не наступит заданное событие или не будет соблюдено некоторое условие. Как правило, программы используют следующие три объекта:

Работать с переменными условия непросто, но зато они позволяют реализовать мощные и эффективные механизмы синхронизации. Дополнительная информация о реализации долговременных блокировок и семафоров с переменными условия приведена в разделе Создание сложных объектов синхронизации.

В переменной условия допускается задавать атрибуты, определяющие характеристики условия. Однако эти атрибуты не поддерживаются в текущей версии AIX. Поэтому при создании переменной условия для AIX атрибуты задавать не обязательно.

Следующие разделы посвящены работе с условными переменными:

Объект атрибутов условия

Так же как нити и взаимные блокировки, переменные условия создают с помощью объекта атрибутов. Объект атрибутов условия - это абстрактный объект, который содержит один атрибут или не содержит ни одного, в зависимости от варианта реализации опций POSIX. Доступ к нему осуществляется через переменную типа 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);
                        /* удаление объекта атрибутов */
}

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

Атрибут условия

В AIX понятие атрибута условия не определено. Атрибуты условия зависят от опций POSIX, которые в AIX не реализованы. Однако в других системах можно задать следующий атрибут:

Process-shared Задает параметры совместного использования процесса переменной условия. Данный атрибут зависит от опций POSIX совместного использования процесса.

Дополнительная информация об атрибутах process-shared приведена в разделе Расширенные атрибуты.

Создание и удаление переменных условия

Для создания переменной условия предназначена функция pthread_cond_init. Вы можете определить объект атрибутов условия. Задав указатель NULL, вы определите атрибуты по умолчанию. Следовательно, следующий фрагмент кода:

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 более одного раза с одним и тем же параметром условия (например, в двух параллельных нитях, выполняющих одну и ту же программу) не рекомендуется. В результате повторного вызова будет возвращен код ошибки EBUSY. Обеспечить однократное создание переменной условия можно следующими тремя способами:

Если переменная условия не нужна, ее следует немедленно уничтожить. Для этого предназначена функция 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              предикат_условия;
        pthread_mutex_t  условная_блокировка;
        pthread_cond_t   переменная_условия;
};

Дополнительная информация приведена в разделе Синхронизация нитей с помощью условных переменных.

Ожидание выполнения условия

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

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

Функция pthread_cond_timedwait блокирует нить только на заданный промежуток времени. У нее есть дополнительный параметр timeout, который указывает дату и время снятия блокировки. Параметр timeout - это указатель на структуру timespec. Этот тип данных еще называют timestruc_t. Он состоит из двух полей:

tv_sec Длинное целое без знака, задающее значение секунд
tv_nsec Длинное целое, задающее значение наносекунд

Типичный вызов функции pthread_cond_timedwait приведен ниже:

struct timespec timeout;
...
time(&timeout.tv_sec);
timeout.tv_sec += МАКС_ПРОДОЛЖИТЕЛЬНОСТЬ_ОЖИДАНИЯ;
pthread_cond_timedwait(&cond, &mutex, &timeout);

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

В случае применения pthread_cond_timedwait с указанием абсолютного времени значение поля tv_sec в структуре timespec можно вычислить с помощью функции mktime. В приведенном ниже примере нить будет ожидать выполнения условия до 08:00 по местному времени 1 января 2001 г.:

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 возвращает при удалении переменной условия. См. следующий пример:

while (pthread_cond_destroy(&cond) == EBUSY) {
        pthread_cond_broadcast(&cond);
        pthread_yield();
}

Функция pthread_yield позволяет запланировать другую нить, например, одну из активизированных. Дополнительная информация о функции pthread_yield приведена в разделе Планирование работы нитей.

Функции pthread_cond_wait и pthread_cond_broadcast нельзя применять вместе с обработчиком сигнала. Для организации ожидания сигнала в библиотеке нитей предусмотрена функция sigwait. Дополнительная информация о функции sigwait приведена в разделе Управление сигналами.

Синхронизация нитей с помощью условных переменных

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

Семантика условия

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

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

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

Рекомендуется вставить функцию ожидания условия в цикл while, проверяющий предикат. Ниже приведен стандартный пример функции ожидания условия.

pthread_mutex_lock(&condition_lock);
while (condition_predicate == 0)
        pthread_cond_wait(&condition_variable, &condition_lock);
...
pthread_mutex_unlock(&condition_lock);

Семантика тайм-аута

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

Как и в случае бесконечного ожидания, нить должна при каждом тайм-ауте проверять, не изменился ли предикат, и определять, следует ли продолжать выполнение или объявить тайм-аут. При завершении функции 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);
}

У такой процедуры есть некоторые недостатки: ее можно применить только один раз и число вызывающих ее нитей обозначено символьной константой. Тем не менее, этот пример отражает основной способ применения переменных условия. Более сложный случай рассмотрен в разделе Создание сложных объектов синхронизации.

Связанная информация

Рекомендации по созданию программ с нитями

Обзор синхронизации

Использование взаимных блокировок

Стыковка нитей

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

Необязательные компоненты библиотеки работы с нитями


[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]