учебники, программирование, основы, введение в,
Новогодняя фотосессия читайте здесь.

 

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

Особенности синхронизации потоков управления
По сравнению с процессами, потоки управления характеризуются двумя особенностями:

  • на порядок меньшими накладными расходами на обслуживание;
  • существенно более тесным взаимодействием в общем адресном пространстве.

Очевидно, чтобы быть практически полезными, средства синхронизации, специально ориентированные на потоки управления, должны быть оптимизированы с учетом обеих отмеченных особенностей.
К числу таких средств, присутствующих в стандарте POSIX-2001, принадлежат мьютексы, переменные условия, блокировки чтение-запись, спин-блокировки и барьеры.
Мьютекс – это синхронизирующий объект, использование которого позволяет множеству потоков управления упорядочить доступ к разделяемым данным. Название этого средства синхронизации отражает его функциональность – взаимное исключение (mutual-exclusion). Поток захватывает мьютекс в монопольное владение и остается владельцем, пока сам же его не освободит.
Переменная условия в качестве синхронизирующего объекта дает потокам управления возможность многократно приостанавливать выполнение, пока некий ассоциированный предикат (условие) не станет истинным. Говорят, что поток, выполнение которого приостановлено на переменной условия, блокирован на этой переменной.
Блокировки чтение-запись (много читателей или один писатель) в каждый момент времени позволяют нескольким потокам управления одновременно иметь к данным доступ на чтение или только одному потокудоступ на запись. Естественно, подобные блокировки обычно применяют для защиты данных, которые читаются чаще, чем изменяются.
Спин-блокировки представляют собой низкоуровневое средство синхронизации. Как и мьютексы, они предназначены для упорядочения доступа множества потоков управления к разделяемым данным.
Барьеры предназначены для синхронизации множества потоков управления в определенной точке их выполнения.
Средства синхронизации могут использоваться для достижения двух существенно разных целей:

  • захват (как правило, на короткое время) разделяемого объекта для защиты критического интервала;
  • ожидание (долгое или даже потенциально неограниченное) наступления некоторого события, выполнения некоторого условия.

Мьютексы и блокировки можно отнести к первой из выделенных категорий, переменные условия и барьеры – ко второй.
Вообще говоря, средства синхронизации потоков управления можно использовать не только в рамках одного процесса, но и среди множества процессов, расположенных в разделяемой, доступной на запись памяти, соответствующим образом инициализированной.
Со средствами синхронизации потоков управления ассоциируются атрибуты и атрибутные объекты, которые можно опрашивать и/или изменять аналогично тому, как это делается для самих потоков.

Мьютексы
Функции, обслуживающие мьютексы, можно разбить на следующие группы:

  • инициализация и разрушение мьютексов: pthread_mutex_init(), pthread_mutex_destroy() (см. листинг 2.1);
  • #include <pthread.h>
  •  
  • int pthread_mutex_init (
  •     pthread_mutex_t *restrict mutex,
  •     const pthread_mutexattr_t
  •         *restrict attr);
  •  
  • int pthread_mutex_destroy (
  •     pthread_mutex_t *mutex);
  •  
  • pthread_mutex_t mutex =
  •     PTHREAD_MUTEX_INITIALIZER;

Листинг 2.1. Описание функций инициализации и разрушения мьютексов.

  • захват и освобождение мьютексов: pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_timedlock(), pthread_mutex_unlock() (см. листинги
  • #include <pthread.h>
  •  
  • int pthread_mutex_lock (
  •     pthread_mutex_t *mutex);
  •  
  • int pthread_mutex_trylock (
  •     pthread_mutex_t *mutex);
  •  
  • int pthread_mutex_unlock (
  •     pthread_mutex_t *mutex);

Листинг 2.2. Описание функций захвата и освобождения мьютексов. (#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock (
pthread_mutex_t *restrict mutex,
const struct
timespec *restrict abstime);
Листинг 2.3. Описание функции захвата мьютексов с ограниченным ожиданием. опрос и установка атрибутов мьютекса: pthread_mutex_getprioceiling(), pthread_mutex_setprioceiling() (см. листинг 2.4);

  • #include <pthread.h>
  •  
  • int pthread_mutex_getprioceiling (
  •     const pthread_mutex_t
  •     *restrict mutex,
  •     int *restrict prioceiling);
  •  
  • int pthread_mutex_setprioceiling (
  •     pthread_mutex_t
  •     *restrict mutex, int prioceiling,
  •     int *restrict old_ceiling);

Листинг 2.4. Описание функций опроса и установки атрибутов мьютекса.

  • инициализация и разрушение атрибутных объектов мьютексов: pthread_mutexattr_init(), pthread_mutexattr_destroy() (см.);
  • #include <pthread.h>
  •  
  • int pthread_mutexattr_init (
  •     pthread_mutexattr_t *attr);
  •  
  • int pthread_mutexattr_destroy (
  •     pthread_mutexattr_t *attr);

Листинг 2.5. Описание функций инициализации и разрушения атрибутных объектов мьютексов.

  • опрос и установка атрибутов мьютекса в атрибутных объектах: pthread_mutexattr_gettype(), pthread_mutexattr_settype(), pthread_mutexattr_getpshared(), pthread_mutexattr_setpshared(), pthread_mutexattr_getprotocol(), pthread_mutexattr_setprotocol(), pthread_mutexattr_getprioceiling(), pthread_mutexattr_setprioceiling()
  • #include <pthread.h>
  •  
  • int pthread_mutexattr_gettype (
  •     const pthread_mutexattr_t
  •         *restrict attr,
  •     int *restrict type);
  •  
  • int pthread_mutexattr_settype (
  •     pthread_mutexattr_t *attr, int type);
  •  
  • int pthread_mutexattr_getpshared (
  •     const pthread_mutexattr_t
  •         *restrict attr,
  •     int *restrict pshared);
  •  
  • int pthread_mutexattr_setpshared (
  •     pthread_mutexattr_t *attr,
  •     int pshared);
  •  
  • int pthread_mutexattr_getprotocol (
  •     const pthread_mutexattr_t
  •         *restrict attr,
  •     int *restrict protocol);
  •  
  • int pthread_mutexattr_setprotocol (
  •     *attr, int protocol);
  •  
  • int pthread_mutexattr_getprioceiling (
  •     const pthread_mutexattr_t
  •         *restrict attr,
  •     int *restrict prioceiling);
  • int pthread_mutexattr_setprioceiling (
  •     pthread_mutexattr_t *attr,
  •     int prioceiling);

Листинг 2.6. Описание функций опроса и установки атрибутов мьютекса в атрибутных объектах. Мы не будем детально описывать каждую из перечисленных выше функций (хотя бы потому, что дисциплина работы с атрибутами и атрибутными объектами та же, что и для потоков управления), но коснемся лишь отдельных, специфических аспектов.
Сразу после инициализации функцией pthread_mutex_init() мьютекс, разумеется, оказывается свободным. Разрушить функцией pthread_mutex_destroy() можно только инициализированный, свободный мьютекс.
Для инициализации статически описанных мьютексов с подразумеваемыми значениями атрибутов целесообразно воспользоваться макросом PTHREAD_MUTEX_INITIALIZER. Эффект будет тем же, что и после вызова pthread_mutex_init() с пустым указателем в качестве значения аргумента attr, только без накладных расходов на проверку корректности атрибутного объекта.
В стандарте POSIX-2001 тип pthread_mutex_t трактуется как абстрактный, со скрытой структурой и даже без методов для присваивания и сравнения на равенство, а попытки обойти их отсутствие за счет применения операций с областями памяти, естественно, обречены на неудачу поскольку, согласно стандарту, для синхронизации должны использоваться сами объекты-мьютексы, а не их копии. Это «развязывает руки» операционной системе в использовании доступных аппаратных возможностей при реализации мьютексов, делая ее максимально эффективной.
У инициализированного мьютекса имеется четыре атрибута:

  • тип (обслуживается функциями pthread_mutexattr_gettype() и pthread_mutexattr_settype());
  • верхняя грань приоритетов выполнения (функции pthread_mutex_getprioceiling(), pthread_mutex_setprioceiling(), pthread_mutexattr_getprioceiling(), pthread_mutexattr_setprioceiling());
  • протокол (pthread_mutexattr_getprotocol(), pthread_mutexattr_setprotocol());
  • признак использования несколькими процессами (pthread_mutexattr_getpshared(), pthread_mutexattr_setpshared()).

Мы уже отмечали важность эффективной реализации для средств синхронизации потоков управления. Вероятно, по этой причине в число атрибутов не включен идентификатор потока-владельца, поскольку его нужно устанавливать и хранить.

В стандарте POSIX-2001 определены четыре типа мьютексов.
PTHREAD_MUTEX_NORMAL
Эффективный, но небезопасный тип. Не проверяется возможность возникновения тупиковой ситуации при захвате подобного мьютекса (например, когда поток управления попытается захватить уже принадлежащий ему мьютекс). Не фиксируются ошибки при освобождении чужого или свободного мьютекса.
PTHREAD_MUTEX_ERRORCHECK
Данный тип обеспечивает выявление ошибочных ситуаций. Упомянутые выше некорректные действия с мьютексами приведут к выдаче кода ошибки. Поскольку, как считается, мьютекс обычно доступен для захвата, а значения атрибутов проверяются лишь тогда, когда поток приходится блокировать, использование атрибутов по сути не влияет на эффективность. В частности, контроль ошибок несколько замедляет нормальное функционирование только при освобождении мьютекса.
PTHREAD_MUTEX_RECURSIVE
Мьютекс со счетчиком и выявлением ошибочных ситуаций. Поток управления может многократно захватить мьютекс, но затем должен столько же раз освободить его.
PTHREAD_MUTEX_DEFAULT
Подразумеваемое значение атрибута «тип». Некорректные действия приводят к неопределенному эффекту. Реализация может отождествить этот тип с одним из вышеописанных.
Атрибут «верхняя грань приоритетов выполнения» определяет минимальный приоритет, с которым будет выполняться критический интервал, охраняемый мьютексом. Чтобы избежать инверсии приоритетов, значение этого атрибута следует сделать не меньшим, чем максимальный из приоритетов потоков, могущих захватить данный мьютекс (отсюда и название атрибута). Диапазон допустимых значений атрибута тот же, что и для приоритетов политики планирования SCHED_FIFO.
Атрибут «протокол» влияет на планирование потока управления во время владения мьютексом. Согласно стандарту, возможных протоколов три.
PTHREAD_PRIO_NONE
Владение мьютексом не влияет на приоритет и планирование потока управления.
PTHREAD_PRIO_INHERIT
Если поток управления, захватив мьютексы с данным протоколом, блокирует более приоритетные потоки, ему присваивается максимальный из приоритетов ждущих потоков. Если во время владения появляются новые ждущие более приоритетные потоки, приоритет владельца должен быть соответственно повышен. Если затем владелец будет блокирован на другом мьютексе с данным протоколом, повышение приоритетов должно быть рекурсивно распространено.
PTHREAD_PRIO_PROTECT
Если поток управления владеет мьютексами данного типа, он выполняется с приоритетом, являющимся максимумом из верхних граней приоритетов выполнения этих мьютексов, независимо от того, ждут ли какие-либо другие потоки их освобождения.
В стандарте оговаривается, что при изменении приоритета, вызванном операциями с мьютексами, подчиняющимися протоколам PTHREAD_PRIO_INHERIT или PTHREAD_PRIO_PROTECT, поток управления не должен перемещаться в хвост очереди планирования.
Если поток управления владеет несколькими мьютексами с разными протоколами, ему назначается приоритет, максимальный из предписанных каждым из протоколов.
Признак использования несколькими процессами мьютекса, в соответствии со стандартом POSIX-2001, может принимать два значения.
PTHREAD_PROCESS_PRIVATE
Мьютекс доступен только для потоков управления, выполняющихся в рамках того же процесса, что и поток, инициализировавший мьютекс. Это значение атрибута является подразумеваемым.
PTHREAD_PROCESS_SHARED
С мьютексом могут работать все потоки управления, имеющие доступ к памяти, в которой мьютекс расположен, даже если эта память разделяется несколькими процессами.
На попытки захвата мьютекса, осуществляемые с применением функций pthread_mutex_lock(), pthread_mutex_trylock() или pthread_mutex_timedlock() значения атрибутов влияют так, как описано выше.
Как правило, поток, вызвавший pthread_mutex_lock(), блокируется, если мьютекс уже захвачен, и ждет его освобождения. В аналогичной ситуации функция pthread_mutex_trylock() немедленно завершается, возвращая код ошибки, а функция pthread_mutex_timedlock() блокируется, пока либо мьютекс не освободится, либо не наступит заданный аргументом abstime момент времени, отсчитываемого по часам CLOCK_REALTIME.
Обычно функцию pthread_mutex_timedlock() вызывают лишь после того, как с помощью pthread_mutex_trylock() выяснили, что мьютекс захвачен. Ограничение времени ожидания делает программу устойчивой к ошибкам, препятствующим освобождению мьютексов, хотя, если считать, что охраняемый критический интервал невелик, данное свойство не имеет особого значения.
В соответствии с общим подходом, если во время ожидания освобождения мьютекса потоку доставляется сигнал, после выхода из функции обработки сигнала оно возобновляется, как если бы и не прерывалось.
Функция pthread_mutex_unlock() обычно освобождает мьютекс. Для мьютексов типа PTHREAD_MUTEX_RECURSIVE вызов pthread_mutex_unlock(), строго говоря, приводит лишь к уменьшению счетчика на единицу, а освобождение происходит только тогда, когда счетчик становится нулевым. Кому достанется мьютекс после освобождения, зависит от политики планирования.
В качестве примера использования мьютексов приведем упрощенную реализацию динамического выделения памяти в многопотоковой среде. На листинге 2.7 показан заголовочный файл, на листинге 2.8 – исходный текст функций выделения и освобождения памяти.
Листинг 2.7. Заголовочный файл g_malloc.h для функций выделения и освобождения памяти в многопотоковой среде.
Листинг 2.8. Исходный текст функций выделения и освобождения памяти в многопотоковой среде.
Основная идея реализации состоит в том, что, помимо начального пула, поддерживаются списки свободных кусков памяти – отдельные списки для небольших размеров (чтобы ускорить их отведение и возврат и уменьшить фрагментацию памяти) и общий список для прочих (больших) кусков. Из общего списка выбирается первый подходящий элемент.
С каждым из трех источников выделения памяти (пул, списки коротких, список больших кусков) ассоциирован свой мьютекс. Цель подобного разделения – минимизировать время владения мьютексом. Только при операциях с общим списком оно может быть большим.
Для библиотечных функций удобны предоставляемые стандартом POSIX-2001 средства инициализации статически описанных мьютексов.
На листинге 2.9 показана тестовая многопотоковая программа, а на листинге 2.10 – возможные результаты ее работы. Видно, что выполнение потоков управления чередуется, и потоки влияют друг на друга.
Листинг 2.9. Пример программы, использующей функции выделения и освобождения памяти в многопотоковой среде.
Листинг 2.10. Возможные результаты работы программы, использующей функции выделения и освобождения памяти в могопотоковой среде.
Мьютексы – весьма подходящее средство для реализации обеда философов. Ниже приведена программа, написанная С.В. Самборским (см. листинг 2.11).
Листинг 2.11. Многопотоковый вариант решения задачи об обедающих философах с использованием мьютексов.

Переменные условия
Пусть имеется некоторый предикат (условие), зависящий от значений переменных, разделяемых несколькими потоками управления. Совместное использование мьютексов, переменных условия и обслуживающих их функций позволяет организовать экономное ожидание состояния истинности этого предиката.
Общая схема применения переменных условия выглядит следующим образом. С разделяемыми переменными, фигурирующими в предикате, ассоциируется мьютекс, который необходимо захватить перед началом проверок. Затем поток управления входит в цикл вида
while (! предикат) {
Ожидание на переменной условия
с освобождением мьютекса.
После успешного завершения
ожидания поток вновь
оказывается владельцем мьютекса.
}
После нормального выхода из цикла проверяемое условие истинно; можно выполнить требуемые действия и освободить мьютекс.
Разблокирование потоков управления, ожидающих на переменной условия, должен обеспечить другой поток, изменивший значения разделяемых переменных и отправивший ждущим соответствующее уведомление. Вообще говоря, не гарантируется, что в момент разблокирования проверяемое условие истинно, поэтому ожидание и следует обертывать в цикл, делая его потенциально многократным.
По логике применения переменные условия напоминают семафоры: одни потоки ждут их перехода в некоторое состояние, другие своими действиями должны этот переход обеспечить и, тем самым, разблокировать ждущих. Есть, однако, и принципиальное отличие. Содержательный предикат, истинность которого является целью ожидания, ассоциируется с семафором неявно и дополнительно не проверяется; считается, что прекращение ожидания само по себе является свидетельством истинности. Это значит, что ответственность за корректность синхронизирующих действий разделяется между оперирующими с семафором потоками управления со всеми вытекающими отсюда последствиями. Для переменных условия прекращение ожидания гарантий истинности предиката не дает, его приходится в явном виде записывать в заголовке цикла, что делает программу устойчивее и упрощает анализ ее корректности.
Отметим также, что все действия с разделяемыми переменными, которые производятся при поддержке переменных условия, осуществляются при захваченном мьютексе и, тем самым, оказываются потоково-безопасными. Если же попытаться дополнить содержательными проверками операции с семафорами, о синхронизации доступа к разделяемым переменным придется позаботиться отдельно.
Переходя к описанию функций, предлагаемых стандартом POSIX-2001 для обслуживания переменных условия, укажем, что их можно разделить на следующие группы:

  • инициализация и разрушение переменных условия: pthread_cond_init(), pthread_cond_destroy() (см. листинг 2.12);
  • #include <pthread.h>
  •  
  • int pthread_cond_init (
  •   pthread_cond_t *restrict cond,
  •   const pthread_condattr_t
  •     *restrict attr);
  •  
  • int pthread_cond_destroy (
  •    pthread_cond_t *cond);
  •  
  • pthread_cond_t cond =
  •   PTHREAD_COND_INITIALIZER;

Листинг 2.12. Описание функций инициализации и разрушения переменных условия.

  • блокирование (ожидание) на переменной условия: pthread_cond_wait(), pthread_cond_timedwait() (см. листинг 2.13);
  • #include <pthread.h>
  •  
  • int pthread_cond_wait (
  •   pthread_cond_t *restrict cond,
  •   pthread_mutex_t *restrict mutex);
  •  
  • int pthread_cond_timedwait (
  •   pthread_cond_t *restrict cond,
  •   pthread_mutex_t *restrict mutex,
  •   struct timespec *restrict abstime);

Листинг 2.13. Описание функций блокирования на переменной условия. разблокирование (прекращение ожидания) потоков управления, блокированных на переменной условия: pthread_cond_broadcast(), pthread_cond_signal() (см. листинг 2.14);

  • #include <pthread.h>
  •  
  • int pthread_cond_broadcast (
  •   pthread_cond_t *cond);
  •  
  • int pthread_cond_signal (
  •   pthread_cond_t *cond);

Листинг 2.14. Описание функций разблокирования потоков управления, блокированных на переменной условия. инициализация и разрушение атрибутных объектов переменных условия: pthread_condattr_init(), pthread_condattr_destroy() (см. листинг 2.15);

  • #include <pthread.h>
  •  
  • int pthread_condattr_init (
  •   pthread_condattr_t *attr);
  •  
  • int pthread_condattr_destroy (
  •   pthread_condattr_t *attr);

Листинг 2.15. Описание функций инициализации и разрушения атрибутных объектов переменных условия.

  • опрос и установка атрибутов переменных условия в атрибутных объектах: признака использования несколькими процессами (обслуживается функциями pthread_condattr_getpshared(), pthread_condattr_setpshared()) и идентификатора часов реального времени, используемых для ограничения ожидания на переменной условия (функции pthread_condattr_getclock(), pthread_condattr_setclock()) (см. листинг 2.16).
  • #include <pthread.h>
  •  
  • int pthread_condattr_getpshared (
  •   const pthread_condattr_t
  •   *restrict attr, int *restrict pshared);
  •  
  • int pthread_condattr_setpshared (
  •   pthread_condattr_t *attr,
  •   int pshared);
  •  
  • int pthread_condattr_getclock (
  •   const pthread_condattr_t
  •   *restrict attr, clockid_t
  •     *restrict clock_id);
  •  
  • int pthread_condattr_setclock (
  •   pthread_condattr_t *attr,
  •   clockid_t clock_id);

Листинг 2.16. Описание функций опроса и установки атрибутов переменных условия в атрибутных объектах.

Далее, как и для мьютексов, мы остановимся только на специфических особенностях переменных условия.
Функции pthread_cond_wait() и pthread_cond_timedwait() блокируют вызывающий поток управления на переменной условия cond. Этот поток должен являться владельцем мьютекса, заданного аргументом mutex.
Одновременно с началом ожидания происходит освобождение мьютекса, что позволяет другим потокам получить доступ к разделяемым переменным, сделать предикат истинным и разблокировать ждущих.
По завершении ожидания на переменной условия cond поток управления вновь становится владельцем мьютекса mutex. Это дает ему возможность проверить истинность предиката и либо продолжить выполнение, выйдя из цикла, либо возобновить ожидание.
Если значение предиката оказалось ложным, ложным называется и разблокирование потока управления. Для функции pthread_cond_wait() оно может объясняться по крайней мере двумя причинами:

  • поток управления, изменивший значения разделяемых переменных и разблокировавший ждущих, не обеспечил истинности предиката;
  • еще один поток, ожидавший на переменной условия cond и получивший управление в первую очередь, нарушил истинность предиката.

Для функции pthread_cond_timedwait() к приведенному перечню добавляется еще и срабатывание контроля по времени. Отметим, впрочем, что и в этом случае мьютекс передается во владение вызывающему потоку управления, а предикат может оказаться истинным (пока срабатывал контроль по времени, другой поток изменил значения разделяемых переменных).
По целому ряду причин контроль по времени целесообразно оформлять с помощью абсолютных, а не относительных значений. Вероятно, главная из них состоит в том, что целью является не успешный возврат из функции pthread_cond_timedwait(), а истинность предиката, то есть завершение цикла, показанного на листинге 2.17.
rc = 0;
while (! predicate && rc == 0) {
rc = pthread_cond_timedwait (
&cond, &mutex, &ts);
}
Если бы аргумент ts задавался как интервал, его пришлось бы перевычислять в цикле; кроме того, контроль по времени оказался бы смазанным непредсказуемыми задержками в выполнении потока управления.
Ожидание на переменной условия является точкой терминирования потока управления. Перед вызовом первого обработчика завершения поток вновь становится владельцем мьютекса, как если бы ожидание завершилось обычным образом, но вместо возврата из функции началось выполнение действий по терминированию, причем в том же состоянии, в каком выполняется код в окрестности обращения к функции. Это делает безопасным доступ обработчиков завершения к разделяемым переменным и вообще упрощает их написание, что способствует уменьшению числа возможных ошибок.
Функция pthread_cond_broadcast() разблокирует все потоки управления, ждущие на переменной условия (это полезно, например, когда писатель уступает место читателям), а функция pthread_cond_signal() (не генерирующая, вопреки названию, никаких сигналов) – по крайней мере один из них (если таковые вообще имеются). Порядок разблокирования определяется политикой планирования; борьба за владение мьютексом, заданным в начале ожидания, протекает так же, как и при одновременном вызове pthread_mutex_lock (mutex).
Вообще говоря, поток управления, вызвавший pthread_cond_broadcast() или pthread_cond_signal(), не обязан быть владельцем упомянутого мьютекса; тем не менее, если требуется предсказуемость решений планировщика, это условие должно быть выполнено.
Реализация должна гарантировать, что поток управления, ожидавший на переменной условия и разблокированный в результате получения заказа на терминирование, не «потребит» разрешение на разблокирование, сгенерированное функцией pthread_cond_signal(), если имеются другие потоки, ожидающие на той же переменной условия. Без подобной гарантии конкуренция между терминированием и разблокированием может завершиться тупиковой ситуацией.
Не рекомендуется вызывать pthread_cond_signal() (а также освобождать мьютексы) в (асинхронно выполняемых) функциях обработки сигналов.
В качестве примера использования переменных условия и мьютексов приведем прямолинейное, но вполне практичное многопотоковое решение задачи об обедающих философах (см. листинг 2.18).
Результаты работы этой программы могут выглядеть так, как показано на листинге 2.19.
Несмотря на то, что в приведенной программе используется глобальный (общий для всех философов и вилок) мьютекс, это не приведет к лишним задержкам, так как мьютекс служит не для захвата вилок, а лишь для проверки и изменения их состояния. Практически всегда он будет свободен.
В употреблении глобальной (в том же смысле, что и мьютекс) переменной условия и всеобщем разблокировании ждущих (посредством вызова pthread_cond_broadcast()) при освобождении любой пары вилок также нет ничего плохого. Ресурсов это практически не отнимает, а пока до разбуженного философа дойдет очередь, нужные ему вилки на самом деле могут оказаться свободными, даже если ожидание на переменной условия было прервано не из-за них. В то же время, кажущееся предпочтительным создание соответствующего числа переменных условия и избирательное разблокирование соседей насытившегося философа двумя вызовами pthread_cond_signal() на самом деле не позволяет гарантировать отсутствие ложных пробуждений, так как из-за нюансов планирования нужную вилку могут выхватить буквально из-под носа (или из-под руки).
Обратим внимание на то, что цикл, в который заключено ожидание на переменной условия, в данном случае использован для захвата нескольких ресурсов. Это напоминает описанные в курсе  групповые операции с семафорами, если иметь в виду цикл в целом и отвлечься от ложных разблокирований.
Разумеется, приведенное решение задачи об обедающих философах является нечестным, поскольку во время ожидания на переменной условия, которое длится неопределенно долго, философ «отключается», он не беседует и не ест, что, по условию задачи, запрещено (этим недостатком страдает и программа из курса [1], основанная на групповых операциях с семафорами). Однако как иллюстрация типичных способов работы с мьютексами и переменными условия данная программа имеет право на существование.


Блокировки чтение-запись
Блокировки чтение-запись можно назвать интеллектуальным средством синхронизации, поскольку они делают различие между читателями и писателями. В большинстве случаев разделяемые данные чаще читают, чем изменяют (действительно, зачем писать то, что никто не будет читать?), и это делает блокировки чтение-запись весьма употребительными.
Типичный пример использования блокировок чтение-записьсинхронизация доступа к буферизованным в памяти фрагментам файловой системы, особенно к каталогам (чем ближе к корню, тем чаще их читают и реже изменяют). Важно отметить в этой связи, что, в отличие от блокировок, реализуемых функцией fcntl() (см. курс [1]), блокировки чтение-запись могут применяться и там, где файловая система отсутствует (например, в минимальных конфигурациях, функционирующих под управлением соответствующей подпрофилю стандарта POSIX операционной системы реального времени).
Применительно к блокировкам чтение-запись предоставляются следующие группы функций.
  • инициализация и разрушение блокировок: pthread_rwlock_init(), pthread_rwlock_destroy() (см. листинг 2.20);
  • #include <pthread.h>
  •  
  • int pthread_rwlock_init (
  •   pthread_rwlock_t
  •     *restrict rwlock,
  •   const pthread_rwlockattr_t
  •     *restrict attr);
  •  
  • int pthread_rwlock_destroy (
  •   pthread_rwlock_t *rwlock);
  • установка блокировки на чтение: pthread_rwlock_rdlock(), pthread_rwlock_tryrdlock(), pthread_rwlock_timedrdlock() (см. листинги
  • #include <pthread.h>
  •  
  • int pthread_rwlock_rdlock (
  •   pthread_rwlock_t *rwlock);
  •  
  • int pthread_rwlock_tryrdlock (
  •   pthread_rwlock_t *rwlock);

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock (
pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abstime);
 

  • установкаблокировкиназапись: pthread_rwlock_wrlock(), pthread_rwlock_trywrlock(), pthread_rwlock_timedwrlock() (см. листинги
  • #include <pthread.h>
  •  
  • int pthread_rwlock_wrlock (
  •   pthread_rwlock_t *rwlock);
  •  
  • int pthread_rwlock_trywrlock (
  •   pthread_rwlock_t *rwlock);

#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedwrlock (
pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict abstime);

  • снятие блокировки чтение-запись: pthread_rwlock_unlock()
  • #include <pthread.h>
  • int pthread_rwlock_unlock (
  •   pthread_rwlock_t *rwlock);
  • инициализация и разрушение атрибутных объектов блокировок: pthread_rwlockattr_init(), pthread_rwlockattr_destroy() (см.);
  • #include <pthread.h>
  •  
  • int pthread_rwlockattr_init (
  •   pthread_rwlockattr_t *attr);
  •  
  • int pthread_rwlockattr_destroy (
  •   pthread_rwlockattr_t *attr);
  • опрос и установка атрибутов блокировок в атрибутных объектах: pthread_rwlockattr_getpshared(), pthread_rwlockattr_setpshared() (см.
  • #include <pthread.h>
  •  
  • int pthread_rwlockattr_getpshared (
  •   const pthread_rwlockattr_t
  •   *restrict attr, int *restrict pshared);
  •  
  • int pthread_rwlockattr_setpshared (
  •   pthread_rwlockattr_t *attr,
  •   int pshared);

Тонким вопросом, связанным с блокировками чтение-запись, является взаимодействие читателей и писателей. Согласно стандарту POSIX-2001, если речь не идет о приложениях реального времени, то от реализации зависит, будет ли приостановлен поток управления при попытке установить блокировку на чтение при наличии ожидающих освобождения той же блокировки писателей. (Как правило, чтобы воспрепятствовать зависанию писателей, читателей в подобной ситуации «тормозят», иначе они так и будут подхватывать блокировку друг у друга. Для политик планирования реального времени преимущество получает более приоритетный поток.)
Естественно, если блокировку установил писатель, читателю придется подождать. Если этим писателем оказался тот же поток, ждать придется долго...
Параллельно одним или несколькими потоками управления может быть установлено несколько блокировок на чтение. Сколько раз устанавливали такую блокировку, столько же раз ее необходимо снять. (Установка блокировки писателем возможна только после последнего снятия.) Максимально допустимое число одновременно установленных блокировок на чтение зависит от реализации.
Когда снимают блокировку, на которую претендуют и читатели, и писатели, как правило, преимущество получают писатели, но, строго говоря, решение зависит от реализации.
Разумеется, снимать блокировку может только тот поток управления, который ее устанавливал; в противном случае поведение неопределено.
Можно видеть, что в данном случае оптимистичное применение блокировок чтение-запись без приостановки выполнения в случае невозможности немедленной установки себя полностью оправдало.

Спин-блокировки
Спин-блокировки представляют собой чрезвычайно низкоуровневое средство синхронизации, предназначенное в первую очередь для применения в многопроцессорной конфигурации с разделяемой памятью. Они обычно реализуются как атомарно устанавливаемое булево значение (истина – блокировка установлена). Аппаратура поддерживает подобные блокировки командами вида «проверить и установить».
При попытке установить спин-блокировку, если она захвачена кем-то другим, как правило, применяется активное ожидание освобождения, с постоянным опросом в цикле состояния блокировки. Естественно, при этом занимается процессор, так что спин-блокировки следует устанавливать только на очень короткое время и их владелец не должен приостанавливать свое выполнение.
Для описываемых блокировок стандарт POSIX-2001 не предусматривает установки с ограниченным ожиданием. Это понятно, поскольку накладные расходы по времени на ограничение в типичном случае превысят само время ожидания.
По сравнению с мьютексами спин-блокировки могут иметь то преимущество, что (активное) ожидание и установка не связаны с переключением контекстов, активизацией планировщика и т.п. Если ожидание оказывается кратким, минимальными оказываются и накладные расходы. Приложение, чувствительное к подобным тонкостям, в каждой конкретной ситуации может выбрать наиболее эффективное средство синхронизации.
Согласно стандарту POSIX-2001, спин-блокировки обслуживаются следующими группами функций:

  • инициализация и разрушение спин-блокировок: pthread_spin_init(), pthread_spin_destroy()
  • #include <pthread.h>
  •  
  • int pthread_spin_init (
  •   pthread_spinlock_t *lock, int pshared);
  •  
  • int pthread_spin_destroy (
  •   pthread_spinlock_t *lock);

Листинг 2.32. Описание функций инициализации и разрушения спин-блокировок.

  • установка спин-блокировки: pthread_spin_lock(), pthread_spin_trylock() (см.
  • #include <pthread.h>
  •  
  • int pthread_spin_lock (
  •   pthread_spinlock_t *lock);
  •  
  • int pthread_spin_trylock (
  •   pthread_spinlock_t *lock);

Листинг 2.33. Описание функций установки спин-блокировки. снятие спин-блокировки: pthread_spin_unlock() (

  • #include <pthread.h>
  • int pthread_spin_unlock (
  •   pthread_spinlock_t *lock);

Листинг 2.34. Описание функции снятия спин-блокировки. (
Обратим внимание на то, что применительно к спин-блокировкам было решено не возиться с атрибутными объектами, а единственный поддерживаемый атрибут – признак использования несколькими процессами – задавать при вызове функции pthread_spin_init().
Поскольку между применением мьютексов и спин-блокировок много общего, мы не будем приводить примеры программ, использующих спин-блокировки. Ограничимся маленьким, слегка модифицированным характерным фрагментом реализации функции sigsuspend() в библиотеке glibc (см.
pthread_spin_lock (&ss->lock);
/* Восстановим старую маску */
ss->blocked = oldmask;
/* Проверим ждущие сигналы */
pending = ss->pending & ~ss->blocked;
pthread_spin_unlock (&ss->lock);
Листинг 2.35. Фрагмент возможной реализации функции sigsuspend(). (
Спин-блокировка устанавливается на очень короткий участок кода; естественно, она должна быть реализована весьма эффективно, чтобы накладные расходы не оказались чрезмерными.

Барьеры
Барьеры – весьма своеобразное средство синхронизации. Идея его в том, чтобы в определенной точке ожидания собралось заданное число потоков управления. Только после этого они смогут продолжить выполнение. (Поговорка «семеро одного не ждут» к барьерам не применима.)
Барьеры полезны для организации коллективных распределенных вычислений в многопроцессорной конфигурации, когда каждый участник (поток управления) выполняет часть работы, а в точке сбора частичные результаты объединяются в общий итог.
Функции, ассоциированные с барьерами, подразделяются на следующие группы.

  • инициализация и разрушение барьеров: pthread_barrier_init(), pthread_barrier_destroy() (см.);
  • #include <pthread.h>
  •  
  • int pthread_barrier_init (
  •   pthread_barrier_t *restrict barrier,
  •     const pthread_barrierattr_t
  •       *restrict attr,
  •       unsigned count);
  •  
  • int pthread_barrier_destroy (
  •   pthread_barrier_t *barrier);

Листинг 2.36. Описание функций инициализации и разрушения барьеров. (

  • синхронизация на барьере: pthread_barrier_wait() (см. include <pthread.h>
  • int pthread_barrier_wait (
  •   pthread_barrier_t *barrier);

Листинг 2.37. Описание функции синхронизации на барьере. (

  • инициализация и разрушение атрибутных объектов барьеров: pthread_barrierattr_init(), pthread_barrierattr_destroy() (см.
  • #include <pthread.h>
  •  
  • int pthread_barrierattr_init (
  •   pthread_barrierattr_t *attr);
  •  
  • int pthread_barrierattr_destroy (
  •   pthread_barrierattr_t *attr);

Листинг 2.38. Описание функций инициализации и разрушения атрибутных объектов барьеров. (

  • опрос и установка атрибутов барьеров в атрибутных объектах: pthread_barrierattr_getpshared(), pthread_barrierattr_setpshared() (см.
  • #include <pthread.h>
  •  
  • int pthread_barrierattr_getpshared
  •       (const pthread_barrierattr_t
  •         *restrict attr,
  •       int *restrict pshared);
  •  
  • int pthread_barrierattr_setpshared
  •       (pthread_barrierattr_t *attr,
  •         int pshared);

Листинг 2.39. Описание функций опроса и установки атрибутов барьеров в атрибутных объектах. (
Обратим внимание на аргумент count в функции инициализации барьера pthread_barrier_init(). Он задает количество синхронизируемых потоков управления. Столько потоков должны вызвать функцию pthread_barrier_wait(), прежде чем каждый из них сможет успешно завершить вызов и продолжить выполнение. (Разумеется, значение count должно быть положительным.)
Когда к функции pthread_barrier_wait() обратилось требуемое число потоков управления, одному из них (стандарт POSIX-2001 не специфицирует, какому именно) в качестве результата возвращается именованная константа PTHREAD_BARRIER_SERIAL_THREAD, а всем другим выдаются нули. После этого барьер возвращается в начальное (инициализированное) состояние, а выделенный поток может выполнить соответствующие объединительные действия.
Описанная схема работы проиллюстрирована
if ((status = pthread_barrier_wait(
&barrier)) ==
PTHREAD_BARRIER_SERIAL_THREAD) {
/* Выделенные (обычно – объединительные) */
/* действия.     */
/* Выполняются каким-то одним потоком */
/* управления */

} else {
/* Эта часть выполняется всеми */
/* прочими потоками */
/* управления */
if (status != 0) {
/* Обработка ошибочной ситуации */
} else {
/* Нормальное "невыделенное" */
/* завершение ожидания */
/* на барьере */
}
}

/* Повторная синхронизация – */
/* ожидание завершения выделенных действий */
status = pthread_barrier_wait (&barrier);
/* Продолжение параллельной работы */
. . .
Листинг 2.40. Типичная схема применения функции pthread_barrier_wait().Отметим, что для барьеров отсутствует вариант синхронизации с контролем времени ожидания. Это вполне понятно, поскольку в случае срабатывания контроля барьер окажется в неработоспособном состоянии (требуемое число потоков, скорее всего, уже не соберется). По той же причине функция pthread_barrier_wait() не является точкой терминирования – «оставшиеся в живых» не переживут потери товарища...
Аналогично, не являются точками терминирования функции pthread_mutex_lock() и pthread_spin_lock(). Если бы они были таковыми, то точками терминирования стали бы все функции, в том числе библиотечные, которые их вызывают – malloc(), free() и т.п. Обеспечить в обработчиках завершения корректное состояние объектов, обслуживаемых подобными функциями, довольно сложно; это деятельность, чреватая ошибками, которые трудно не только найти и исправить, но даже воспроизвести. С другой стороны, мьютексы и спин-блокировки предназначены для захвата на короткое время, без длительного ожидания, так что нечувствительность к терминированию в данном случае не составляет большой проблемы.
Работу с барьерами проиллюстрируем коллективными вычислениями, производимыми двумя потоками (см.
Листинг 2.41. Пример программы, использующей барьеры. (
В данном случае второго ожидания на барьере не понадобилось – вместо этого потоки управления просто завершаются.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика