Олег Цилюрик - QNX/UNIX: Анатомия параллелизма
# nice -n-19 a1 -n10000000 -a
all cycles — 391018203; on operation - 39
Результат с применением атомарной операции, возвращающей значение (обещанная разница составляет порядка 10%):
# nice -n-19 a1 -n10000000 -a -v
all cycles - 441158981; on operation - 44
Условная переменная
Одним из важнейших принципов использования мьютексов является максимальное сокращение размеров критической секции, то есть участка, который потоки должны проходить последовательно. Однако зачастую возникает необходимость ожидания выполнения некоторого условия внутри критической секции.
Реализация подобного ожидания «в лоб» привела бы к тому, что все потоки, разделяющие данную критическую секцию, были бы вынуждены ждать выполнения условия для каждого из них. При «правильной» реализации ожидания поток должен освобождать мьютекс на время ожидания и вновь захватывать его, когда ожидаемое условие выполняется. Специально для этого случая стандартом POSIX предусмотрены условные переменные. QNX Neutrino реализует условные переменные как на уровне вызовов микроядра в своем native API, так и в соответствии со стандартом POSIX.
Отметим, что дополнение мьютекса условной переменной делает их комбинацию универсальным базисом, на котором могут быть построены любые сколь угодно сложные в своем поведении объекты синхронизации.
Фактически совместное использование мьютекса и условной переменной создает специфический комбинированный объект синхронизации, который может иметь принципиально более широкое применение, чем отдельно взятый мьютекс. Тем не менее поведение этого объекта синхронизации не столь просто и далеко не очевидно. Рассмотрим его более подробно.
На рис. 4.1 приведена блок-схема операций, выполняемых потоком при использовании мьютекса и условной переменной для синхронизации. Линиями отделены операции, выполняющиеся «внутри» функций, указанных справа. Обратите внимание, что наиболее сложная логика соответствует вызову функции ожидания на условной переменной.
Рис. 4.1. Схема действий потока при выполнении синхронизации с применением пары мьютекс-условная переменная (обратите внимание, что операции при участии мьютекса (1, 2, 3) выполняются дважды.)
Проблема в первую очередь заключается в том, что внутри критической секции, отмеченной вызовами функций pthread_mutex_lock() и pthread_mutex_unlock(), не может находиться более одного потока в единый момент времени. Следовательно, даже если поток, блокированный на условной переменной, и получит pthread_cond_signal() или pthread_cond_broadcast(), он не сможет немедленно продолжить свое выполнение, если внутри критической секции уже находится другой поток. В этом случае разблокированный (на условной переменной) поток изменяет свой статус с «блокированного на условной переменной» (в котором он находился до этого) на статус «блокированного на мьютексе» и пребывает в нем до тех пор, пока текущий владелец не освободит мьютекс.
Все функции операций над условной переменной и ее атрибутами реализованы в заголовочном файле <pthread.h>. Если для вызова функции необходимы дополнительные заголовочные файлы, это будет указано особо.
Операции над условной переменной
Параметры условной переменной
Инициализация параметровint pthread_condattr_init(pthread_condattr_t* attr);
Функция инициализирует структуру атрибутов условной переменной, на которую указывает параметр attr. Структура данных pthread_condattr_t определена в файле <pthread.h> и является производной от типа syncattr_t, описанного в разделе «Параметры мьютекса». При инициализации атрибуты устанавливаются в значения по умолчанию.
Возвращаемые значения:
EOK — успешное завершение;
ENOMEM — недостаточно памяти для инициализации атрибутов условной переменной attr.
Для условной переменной возможна модификация значительно меньшего числа параметров, чем для мьютекса. Следующие функции описывают доступ к этим параметрам.
Параметр доступаint pthread_condattr_setpshared(
pthread_condattr_t* attr, int pshared);
int pthread_condattr_getpshared(
const pthread_condattr_t* attr, int* pshared);
Функции устанавливают/считывают, возможен ли доступ к условной переменной из потоков, порожденных в других процессах. Параметр pshared может принимать следующие значения:
• PTHREAD_PROCESS_SHARED — любой поток, имеющий доступ к области памяти, в которой расположена условная переменная, может ее использовать.
• PTHREAD_PROCESS_PRIVATE (значение по умолчанию) — только поток, созданный в контексте того же процесса, в котором находится условная переменная, может ее использовать. Если поток из другого процесса попытается использовать условную переменную, созданную с параметром PTHREAD_PROCESS_PRIVATE, результат будет не определен.
Возвращаемые значения:
EOK — успешное завершение;
EINVAL — атрибуты условной переменной, на которые указывает attr, или новое значение, на которое ссылается pshared, не определены.
Параметры тайм-аутаint pthread_condattr_setclock(
pthread_condattr_t* attr, clockid_t id);
int pthread_condattr_getclock(
const pthread_condattr_t* attr, clockid_t* id);
Функции устанавливают/считывают, каким способом (т.e. на основании какого счетчика) вычисляется значение тайм-аута при вызовах pthread_cond_timedwait(). Этот параметр в QNX Neutrino 6.2 не является обязательным и введен в соответствии со стандартом POSIX 1003.1j (draft). На практике можно устанавливать только значение REALTIME_CLOCK в качестве параметра id; это же значение является значением по умолчанию.
Возвращаемые значения:
EOK — успешное завершение;
EINVAL — неверный аргумент attr.
Разрушение блока параметров
int pthread_condattr_destroy(pthread_condattr_t* attr);
Функция разрушает блок параметров условной переменной, на которые указывает attr, после чего он уже не может использоваться без повторной инициализации.
На практике разрушение параметров объекта синхронизации не имеет особого смысла. Вы всегда можете переопределить атрибуты, содержащиеся в переменной attr, для инициализации других условных переменных.
Возвращаемые значения:
EOK — успешное завершение;
EINVAL — неверный аргумент attr.
Инициализация условной переменной
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);
Инициализирует условную переменную cond со значениями, установленными атрибутами attr. Вместо прямого вызова функции pthread_cond_init() для начальной инициализации статических условных переменных (глобальных на уровне файла кода или пространства имен namespace либо явно описанных с квалификатором static) можно воспользоваться макросом PTHREAD_COND_INITIALIZER.
Возвращаемые значения:
EOK — успешное завершение;
EAGAIN — нет свободных системных объектов синхронизации;
EBUSY — переменная cond уже инициализирована и не разрушалась;
EFAULT — ошибка доступа ядра к объектам cond или attr;
EINVAL — неправильное значение переменной cond.
Ожидание условия
Простое ожиданиеint pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
Вызов функции блокирует вызвавший поток на условной переменной cond и разблокирует мьютекс mutex. Поток блокируется до тех пор, пока другой поток не вызовет функцию разблокирования на условной переменной cond (pthread_cond_signal() или pthread_cond_broadcast()). Мьютекс mutex должен быть захвачен потоком до вызова функции. Поток, блокированный на условной переменной, может быть разблокирован также приходом сигнала или вызовом завершения потока. В любом случае при разблокировании потока и выходе из функции ожидания поток вновь захватывает мьютекс mutex.
Не следует использовать условную переменную с мьютексом, у которого разрешен рекурсивный захват.
Возвращаемые значения:
EOK — успешное завершение ожидания либо ожидание прервано сигналом;
EAGAIN — недостаток системных ресурсов для реализации ожидания на условной переменной;
EFAULT — произошла ошибка при попытке обращения к указателям cond или mutex;
EINVAL — возвращается в следующих ситуациях:
• не инициализированы переменные, на которые указывают cond или mutex;
• попытка использования переменной, на которую указывает cond, для нескольких мьютексов;