Добавлен: 29.10.2018
Просмотров: 48027
Скачиваний: 190
2.3. Взаимодействие процессов
161
этот буфер опустеет. Эти семафоры используются совсем не так, как при обеспечении
взаимного исключения.
2.3.6. Мьютексы
Иногда при невостребованности возможностей семафоров в качестве счетчиков исполь-
зуется их упрощенная версия, называемая мьютексом. Мьютексы справляются лишь
с управлением взаимным исключением доступа к общим ресурсам или фрагментам
кода. Простота и эффективность реализации мьютексов делает их особенно полезными
для совокупности потоков, целиком реализованных в пользовательском пространстве.
Мьютекс
— это совместно используемая переменная, которая может находиться в од-
ном из двух состояний: заблокированном или незаблокированном. Следовательно,
для их представления нужен только один бит, но на практике зачастую используется
целое число, при этом нуль означает незаблокированное, а все остальные значения —
заблокированное состояние. Для работы с мьютексами используются две процедуры.
Когда потоку (или процессу) необходим доступ к критической области, он вызывает
процедуру mutex_lock. Если мьютекс находится в незаблокированном состоянии
(означающем доступность входа в критическую область), вызов проходит удачно
и вызывающий поток может свободно войти в критическую область.
В то же время, если мьютекс уже заблокирован, вызывающий поток блокируется до тех
пор, пока поток, находящийся в критической области, не завершит свою работу и не
вызовет процедуру mutex_unlock. Если на мьютексе заблокировано несколько потоков,
то произвольно выбирается один из них, которому разрешается воспользоваться за-
блокированностью других потоков.
Благодаря исключительной простоте мьютексов они легко могут быть реализованы
в пользовательском пространстве при условии доступности команды TSL или XCHG.
В листинге 2.7 показан код процедур mutex_lock и mutex_unlock, предназначенных для
использования в совокупности потоков, работающих в пользовательском пространстве.
Решение, в котором используется команда XCHG, по сути, ничем не отличается.
Листинг 2.7. Реализация mutex_lock и mutex_unlock
mutex_lock:
TSL REGISTER,MUTEX | копирование мьютекса в регистр и установка
| его в 1
CMP REGISTER,#0 | был ли мьютекс нулевым?
JZE ok | если он был нулевым, значит, не был
| заблокирован,поэтому нужно вернуть
| управление вызывающей программе
CALL thread_yield | мьютекс занят; пусть планировщик
| возобновит работу другого потока
JMP mutex lock | повторная попытка
ok: RET | возврат управления вызывающей программе;
| будет осуществлен вход в критическую
| область
mutex_unlock:
MOVE MUTEX,#0 | сохранение в мьютексе значения 0
RET | возврат управления вызывающей программе
162
Глава 2. Процессы и потоки
Код процедуры mutex_lock похож на код enter_region в листинге 2.2, но с одной суще-
ственной разницей. Когда процедуре enter_region не удается войти в критическую об-
ласть, она продолжает повторное тестирование значения переменной lock (выполняет
активное ожидание). По истечении определенного времени планировщик возобновляет
работу какого-нибудь другого процесса. Рано или поздно возобновляется работа про-
цесса, удерживающего блокировку, и он ее освобождает.
При работе с потоками (в пользовательском пространстве) складывается несколько
иная ситуация, связанная с отсутствием таймера, останавливающего работу слишком
долго выполняющегося процесса. Следовательно, поток, пытающийся воспользоваться
блокировкой, находясь в состоянии активного ожидания, войдет в бесконечный цикл
и никогда не завладеет блокировкой, поскольку он никогда не позволит никакому
другому потоку возобновить выполнение и снять блокировку.
Вот в этом и заключается разница между enter_region и mutex_lock. Когда последняя из
этих процедур не может завладеть блокировкой, она вызывает процедуру thread_ yield,
чтобы уступить центральный процессор другому потоку. Следовательно, активное
ожидание отсутствует. Когда поток в очередной раз возобновит свою работу, он снова
проверяет состояние блокировки.
Поскольку процедура thread_ yield представляет собой всего лишь вызов планировщика
потоков в пользовательском пространстве, она работает очень быстро. Следовательно,
ни mutex_lock, ни mutex_unlock не требуют никаких вызовов ядра. Благодаря их исполь-
зованию потоки, работающие на пользовательском уровне, могут синхронизироваться
целиком в пространстве пользователя с использованием процедур, для реализации
которых требуется совсем небольшая группа команд.
Рассмотренная ранее система мьютексов составляет основу набора вызовов. Но про-
граммному обеспечению всегда требуется что-то большее, и примитивы синхронизации
здесь не исключение. Например, иногда совокупности потоков предлагается вызов
процедуры mutex_trylock, которая либо овладевает блокировкой, либо возвращает код
отказа, но не вызывает блокировку. Этот вызов придает потоку возможность гибко
решать, что делать дальше, если есть альтернативы простому ожиданию.
До сих пор оставалась еще одна слегка замалчиваемая нами проблема, о которой
все же стоит упомянуть. Пока речь идет о совокупности потоков, реализованных
в пользовательском пространстве, проблем с совместным доступом нескольких по-
токов к одному и тому же мьютексу не возникает, поскольку потоки выполняются
в общем адресном пространстве. Но в большинстве предыдущих решений, например
в алгоритме Петерсона и семафорах, было невысказанное предположение, что не-
сколько процессов имеют доступ по крайней мере к какому-то объему общей памяти,
возможно, всего лишь к одному слову памяти, но все же имеют. Если у процессов
разоб щенные адресные пространства, о чем мы неизменно упоминали, то как они
будут совместно использовать переменную turn в алгоритме Петерсона, или сема-
форы, или общий буфер?
На этот вопрос есть два ответа. Во-первых, некоторые общие структуры данных, на-
пример семафоры, могут храниться в ядре и быть доступны только через системные
вызовы. Такой подход позволяет устранить проблему. Во-вторых, большинство со-
временных операционных систем (включая UNIX и Windows) предлагают процессам
способ, позволяющий использовать некоторую часть их адресного пространства со-
вместно с другими процессами. В этом случае допускается совместное использование
2.3. Взаимодействие процессов
163
буферов и других структур данных. В худшем случае, когда нет доступа ни к чему
другому, можно воспользоваться общим файлом.
Если два или более процесса совместно используют все свои адресные пространства
или их большие части, различие между процессами и потоками немного размывается,
но все равно присутствует. Два процесса, использующие общее адресное пространство,
все равно имеют различные открытые файлы, аварийные таймеры и другие присущие
процессам отличительные свойства, а вот для потоков в рамках одного процесса эти
свойства являются общими. И никуда не деться от того обстоятельства, что несколько
процессов, использующих общее адресное пространство, никогда не будут столь же
эффективными, как потоки, реализованные на пользовательском уровне, поскольку
к управлению процессами неизменно привлекается ядро.
Фьютексы
С ростом параллелизма очень важное значение для производительности приобретают
эффективная синхронизация и блокировка. Спин-блокировки обладают быстротой
при недолгом ожидании, но если ожидание затянется, они будут тратить циклы цен-
трального процессора впустую. При высокой конкуренции более эффективным реше-
нием будет блокировать процесс и позволить ядру разблокировать его, только когда
блокировка будет свободна. К сожалению, возникает обратная проблема: это хорошо
работает в условиях высокой конкуренции, но если конкуренция с самого начала не-
высока, постоянные переключения в режим ядра обходятся слишком дорого. Хуже
того, предсказать количество конкурентных блокировок может быть весьма нелегко.
Одним из интересных решений, пытающихся объединить все самое лучшее из обоих
миров, является так называемый фьютекс, или fast user space mutex, — быстрый мьютекс
в пользовательском пространстве. Фьютекс относится к свойствам Linux, реализующим
основную блокировку (во многом похожую на мьютекс), но избегающим выпадения
в режим ядра до возникновения в этом реальной надобности. Поскольку переключение
в режим ядра и обратно обходится слишком дорого, применение такой технологии суще-
ственно повышает производительность. Фьютекс состоит из двух частей: службы ядра
и пользовательской библиотеки. Служба ядра предоставляет «очередь ожидания», по-
зволяющую ожидать снятия блокировки нескольким процессам. Они не будут запущены,
пока ядро не разблокирует их явным образом. Чтобы процесс попал в очередь ожидания,
требуется (довольно дорого обходящийся) системный вызов, чего следует избегать.
Зато при отсутствии конкуренции фьютекс работает полностью в пользовательском
пространстве. Говоря конкретнее, процессы совместно используют общую переменную
блокировки, являющуюся вымышленным названием для выровненного 32-разрядного
целочисленного значения, которое служит в качестве блокировки.
Предположим, что исходное значение блокировки равно 1, и под этим подразумева-
ется, что блокировка свободна. Поток захватывает блокировку, проводя атомарное
«уменьшение на единицу и тестирование» (атомарные функции в Linux состоят из
встроенного ассемблерного кода, заключенного в функции языка C, и определены в за-
головочных файлах). Затем поток анализирует результат, выясняя, была ли блокировка
свободна. Если она была в незаблокированном состоянии, все обходится благополучно
и наш поток успешно захватывает блокировку. Но если блокировка удерживается
другим потоком, наш поток вынужден ждать. В таком случае библиотека фьютекса не
обращается к спину, а использует системный вызов для помещения потока в очередь
164
Глава 2. Процессы и потоки
ожидания в пространстве ядра. Есть надежда на то, что затраты на переключение в ре-
жим ядра теперь оправданны, поскольку поток все равно был бы заблокирован. Когда
поток, захвативший блокировку, выполнит свою задачу, он освободит блокировку,
проводя атомарное увеличение на единицу и тестирование и проверяя результат, что-
бы увидеть, есть ли процессы, заблокированные на очереди ожидания в пространстве
ядра. Если таковые имеются, он даст ядру понять, что оно может разблокировать один
или несколько таких процессов. Если же конкуренция отсутствует, ядро вовлекаться
в работу вообще не будет.
Мьютексы в пакете Pthreads
Пакет Pthreads предоставляет ряд функций, которые могут быть использованы для
синхронизации потоков. Основным механизмом является использование перемен-
ных — мьютексов, предназначенных для защиты каждой критической области. Каждая
такая переменная может быть в заблокированном или незаблокированном состоянии.
Поток, которому необходимо войти в критическую область, сначала пытается заблоки-
ровать соответствующий мьютекс. Если мьютекс не заблокирован, поток может войти
в критическую область беспрепятственно и заблокировать мьютекс в одном неделимом
действии, не позволяя войти в нее другим потокам. Если мьютекс уже заблокирован,
вызывающий поток блокируется до тех пор, пока не разблокируется мьютекс. Если
разблокировки одного и того же мьютекса ожидают несколько потоков, возможность
возобновить работу и заново заблокировать мьютекс предоставляется только одному
из них. Эти блокировки не являются обязательными. Соблюдение порядка их исполь-
зования потоками всецело возложено на программиста.
Основные вызовы, связанные с мьютексами, показаны в табл. 2.6. Как и ожидалось,
мьютексы могут создаваться и уничтожаться. Вызовы, осуществляющие эти операции,
называются, соответственно, pthread_mutex_init и pthread_mutex_destroy. Мьютексы
также могут быть заблокированы вызовом pthread_mutex_lock, который пытается за-
владеть блокировкой и блокирует выполнение потока, если мьютекс уже заблокирован.
Есть также вызов, используемый для попытки заблокировать мьютекс и безуспешного
выхода с кодом ошибки, если мьютекс уже был заблокирован. Этот вызов называет-
ся pthread_mutex_trylock. Он позволяет потоку организовать эффективное активное
ожидание, если в таковом возникнет необходимость. И наконец, вызов pthread_mutex_
unlock разблокирует мьютекс и возобновляет работу только одного потока, если име-
ется один или более потоков, ожидающих разблокирования. Мьютексы могут иметь
также атрибуты, но они используются только для решения специализированных задач.
Таблица 2.6. Ряд вызовов пакета Pthreads, имеющих отношение к мьютексам
Вызов из потока
Описание
pthread_mutex_init
Создание мьютекса
pthread_mutex_destroy
Уничтожение существующего мьютекса
pthread_mutex_lock
Овладение блокировкой или блокирование потока
pthread_mutex_trylock
Овладение блокировкой или выход с ошибкой
pthread_mutex_unlock
Разблокирование
В дополнение к мьютексам пакет Pthreads предлагает второй механизм синхрониза-
ции — условные переменные. Мьютексы хороши для разрешения или блокирования
2.3. Взаимодействие процессов
165
доступа к критической области. Условные переменные позволяют потокам блоки-
роваться до выполнения конкретных условий. Эти два метода практически всегда
используются вместе. Теперь давайте более пристально взглянем на взаимодействие
потоков, мьютексов и условных переменных.
В качестве простого примера еще раз рассмотрим сценарий производителя-потре-
бителя: один поток что-то помещает в буфер, а другой это что-то из него извлекает.
Если производитель обнаружил отсутствие в буфере свободных мест, он вынужден
блокироваться до тех пор, пока они не появятся. Мьютексы предоставляют возмож-
ность производить проверку атомарно, без вмешательства со стороны других потоков,
но обнаружив, что буфер заполнен, производитель нуждается в способе блокировки
с последующей активизацией. Именно этот способ и предоставляется условными
переменными.
Наиболее важные вызовы, связанные с условными переменными, показаны в табл. 2.7.
Согласно вашим возможным ожиданиям, в ней представлены вызовы, предназначен-
ные для создания и уничтожения условных переменных. У них могут быть атрибуты,
для управления которыми существует ряд других (не показанных в таблице) вызовов.
Первичные операции над условными переменными осуществляются с помощью вы-
зовов pthread_cond_wait и pthread_cond_signal. Первый из них блокирует вызывающий
поток до тех пор, пока не будет получен сигнал от другого потока (использующего
второй вызов). Разумеется, основания для блокирования и ожидания не являются
частью протокола ожиданий и отправки сигналов. Заблокированный поток зачастую
ожидает, пока сигнализирующий поток не совершит определенную работу, не освобо-
дит какие-то ресурсы или не выполнит какие-нибудь другие действия. Только после
этого заблокированный поток продолжает свою работу. Условные переменные по-
зволяют осуществлять это ожидание и блокирование как неделимые операции. Вызов
pthread_cond_broadcast используется в том случае, если есть потенциальная возмож-
ность находиться в заблокированном состоянии и ожидании одного и того же сигнала
сразу нескольким потокам.
Таблица 2.7. Ряд вызовов пакета Pthreads, имеющих отношение
к условным переменным
Вызов из потока
Описание
pthread_cond_init
Создание условной переменной
pthread_cond_destroy
Уничтожение условной переменной
pthread_cond_wait
Блокировка в ожидании сигнала
pthread_cond_signal
Сигнализирование другому потоку и его активизация
pthread_cond_broadcast
Сигнализирование нескольким потокам и активизация всех этих
потоков
Условные переменные и мьютексы всегда используются вместе. Схема для одного пото-
ка состоит в блокировании мьютекса, а затем в ожидании на основе значения условной
переменной, если поток не может получить то, что ему требуется. Со временем другой
поток подаст ему сигнал, и он сможет продолжить работу. Вызов pthread_cond_wait
осуществляется неделимо и выполняет разблокирование удерживаемого мьютекса
как одну неделимую и непрерываемую операцию. По этой причине мьютекс является
одним из его аргументов.