- •Часть 4. Локальное взаимодействие процессов
- •Глава 16. Блокирование записей 89
- •12.2. Процессы, потоки и общий доступ к информации
- •12.3. Живучесть объектов ipc
- •12.4. Пространства имен
- •12.5. Действие команд fork, exec и exit на объекты ipc
- •12.6. Комментарии к примерам ipc
- •12.7. Выводы по главе 12
- •12.8. Упражнения по главе 12
- •Глава 13. Именованные и неименованные каналы
- •13.1. Введение
- •13.2. Приложение типа клиент-сервер
- •13.3. Программные каналы
- •13.4. Функции popen и pclose
- •13.5. Именованные каналы (fifo)
- •13.6. Некоторые свойства именованных и неименованных каналов
- •13.7. Один сервер, несколько клиентов
- •13.8. Последовательные и параллельные серверы
- •13.9. Ограничения программных каналов и fifo
- •13.10. Выводы по главе 13
- •13.11. Упражнения по главе 13
- •Глава 14. Программные потоки
- •14.1. Введение
- •14.2. Концепция потоков
- •14.3. Идентификация потоков
- •14.4. Создание потока
- •14.5. Завершение потока
- •Функции управления процессами и потоками
- •14.6. Установка атрибутов потока
- •14.7. Реентерабельность
- •Альтернативные версии функций, безопасные в многопоточной среде
- •14.8. Локальные данные потоков
- •14.9. Принудительное завершение потоков
- •Некоторые точки выхода, определенные стандартом Posix.1
- •14.10. Потоки и сигналы
- •14.11. Выводы по главе 14
- •14.12. Упражнения по главе 14 Глава 15. Средства синхронизации потоков
- •15.1. Введение
- •15.2. Взаимные исключения: установка и снятие блокировки
- •15.2.1. Схема производитель-потребитель
- •15.2.2. Блокирование и опрос
- •15.2.3. Предотвращение тупиковых ситуаций
- •15.3. Условные переменные
- •15.3.1. Ожидание и сигнализация
- •15.3.2. Исключение состояния гонок
- •15.4. Блокировки чтения-записи
- •15.5. Атрибуты средств синхронизации потоков
- •15.5.1. Атрибуты взаимных исключений
- •Поведение взаимных исключений различных типов
- •15.5.2. Атрибуты условных переменных
- •15.5.3. Атрибуты блокировок чтения-записи
- •15.6. Выводы по главе 15
- •15.7. Упражнения по главе 15
- •Глава 16. Блокирование записей
- •16.1. Введение
- •16.2. Блокирование записей и файлов
- •16.3. Блокирование записей с помощью fcntl по стандарту Posix
- •16.4. Рекомендательная блокировка
- •16.5. Обязательная блокировка
- •16.6. Приоритет чтения и записи Выводы по главе 16
- •Упражнения по главе 16 Глава 17. System V ipc
- •17.1. Введение
- •17.2. Ключи типа key_t и функция ftok
- •17.3. Структура ipc_perm
- •17.4. Создание и открытие каналов ipc
- •17.5. Разрешения ipc
- •17.6. Программы ipcs и ipcrm
- •17.7. Ограничения ядра
- •17.8. Выводы по главе 17
- •17.9. Упражнения по главе 17
- •Глава 18. Очереди сообщений System V
- •18.1. Введение
- •18.2. Функция msgget
- •18.3. Функция msgsnd
- •18.4. Функция msgrcv
- •18.5. Функция msgctl
- •18.6. Пример программы клиент-сервер
- •18.7. Мультиплексирование сообщений
- •18.7.1. Пример: одна очередь на приложение
- •18.7.2. Пример: одна очередь для каждого клиента
- •18.8. Ограничения, накладываемые на очереди сообщений
- •18.9. Выводы по главе 18
- •18.10. Упражнения по главе 18
- •Глава 19. Семафоры System V
- •19.1. Введение
- •19.2. Функция semget
- •19.3. Функция semop
- •19.4. Функция semctl
- •19. . Ограничения семафоров System V
- •19. . Выводы по главе 19
- •19. . Упражнения по главе 19 Глава 20. Введение в разделяемую память
- •20.1. Введение
- •20.2. Функции mmap, munmap и msync
- •20.3. Увеличение счетчика в отображаемом в память файле
- •20.4. Неименованное отображение в память
- •20.5. Обращение к объектам, отображенным в память
- •20.6. Выводы по главе 20
- •20.7. Упражнения по главе 20
- •Глава 21. Разделяемая память System V
- •21.1. Введение
- •21.2. Функция shmget
- •21.3. Функция shmat
- •21.4. Функция shmdt
- •21.5. Функция shmctl
- •21.6. Ограничения, накладываемые на разделяемую память
- •21.7. Выводы по главе 21
- •21.8. Упражнения по главе 21
14.11. Выводы по главе 14
В этой главе мы обсудили концепцию потоков и примитивы Posix.1 для работы с ними. Потоки в системе Unix предоставляют дополнительную возможность декомпозиции сложной задачи на подзадачи. Мы рассмотрели вопросы, связанные с настройкой поведения потоков, и реентерабельность функций относительно потоков. Потоки совместно используют одни и те же общие данные, что, в свою очередь, порождает специфические проблемы синхронизации, которые мы рассмотрим в следующей главе.
14.12. Упражнения по главе 14 Глава 15. Средства синхронизации потоков
15.1. Введение
Эта глава начинается с обсуждения синхронизации – согласованных действий нескольких программных потоков или процессов. Обычно это требуется для предоставления нескольким потокам или процессам совместного доступа к данным. При наличии нескольких потоков управления, совместно использующих одни и те же данные, необходимо гарантировать, что каждый из потоков будет видеть эти данные в непротиворечивом состоянии. Если каждый из потоков использует переменные, которые не используются в других потоках, то проблем не возникает. Аналогично, если переменная доступна одновременно нескольким потокам только для чтения, то здесь также отсутствует проблема сохранения непротиворечивости. Однако если один поток изменяет значение переменной, читать или изменять значение которой могут также и другие потоки, то необходимо синхронизировать доступ к переменной, чтобы гарантировать, что потоки не будут получать неверное значение переменной при одновременном доступе к ней.
Когда поток изменяет значение переменной, существует потенциальная опасность, что другой поток может прочитать еще не до конца записанное значение. На аппаратных платформах, где запись в память осуществляется более чем за один цикл, может произойти так, что между двумя циклами записи вклинится цикл чтения. Разумеется, такое поведение во многом зависит от аппаратной архитектуры, но при написании переносимых программ мы не можем полагаться на то, что они будут выполняться только на определенной платформе.
На рис. 15.1 приводится пример гипотетической ситуации, когда два потока одновременно выполняют запись и чтение значения одной и той же переменной. В данном примере поток A считывает значение переменной и затем записывает в нее новое значение, но операция записи производится за два цикла. Если поток B прочитает значение этой переменной между двумя циклами записи, он обнаружит переменную в противоречивом состоянии.

рис. 15.1
Для решения этой проблемы потоки должны использовать блокировки, которые позволят только одному потоку работать с переменной в один момент времени. На рис. 15.2 показан пример подобной синхронизации. Если поток B должен прочитать значение переменной, он устанавливает блокировку. Аналогичным образом, когда поток A изменяет значение переменной, он также устанавливает блокировку. Таким образом, поток B не сможет прочитать значение переменной, пока поток A не снимет блокировку.

рис. 15.2
Точно так же следует синхронизировать свои действия двум или более потокам, которые могут попытаться одновременно изменить значение переменной. Рассмотрим случай, когда значение переменной увеличивают на 1 (рис. 15.3). Операцию увеличения обычно можно разбить на три шага:
Прочитать значение переменной из памяти в регистр процессора.
Увеличить значение в регистре.
Записать новое значение из регистра процессора в память.
Если два потока попытаются одновременно увеличить значение одной и той же переменной, не согласовывая своих действий между собой, то результаты могут быть получены самые разные. В конечном итоге полученное значение может оказаться на 1 или на 2 больше предыдущего в зависимости от того, какое значение получил второй поток перед началом операции. Если второй поток выполнил шаг 1 до того, как первый поток выполнил шаг 3, то второй поток прочитает то же самое значение, что и первый поток, увеличит его на 1 и запишет обратно в память, фактически не оказав никакого влияния на значение переменной.

рис. 15.3
Если изменение значения переменной производится атомарно, то подобная гонка между потоками отсутствует. В предыдущем примере, если для увеличения значения достаточно одного обращения к памяти, состояния гонки между потоками не возникнет. Если данные постоянно находятся в непротиворечивом состоянии, то в дополнительной синхронизации нет необходимости. Операции являются последовательно непротиворечивыми, если различные потоки не могут получить доступа к данным, когда данные находятся в противоречивом состоянии. В современных компьютерных системах доступ к памяти осуществляется за несколько тактов шины, а в многопроцессорных системах доступ к шине вообще чередуется между несколькими процессорами, поэтому невозможно гарантировать нахождения данных в непротиворечивом состоянии в любой произвольный момент времени.
В непротиворечивой среде можно описать изменения данных как последовательность операций, выполняемых потоками. Мы можем сказать: “Поток A увеличил значение переменной на 1, затем поток B увеличил значение переменной на 1, в результате чего значение переменной было увеличено на 2” или: “Поток B увеличил значение переменной на 1, затем поток A увеличил значение переменной на 1, в результате чего значение переменной было увеличено на 2”. Конечный результат не зависит от того или иного порядка выполнения потоков.
Помимо особенностей аппаратной архитектуры, состояние гонки может быть вызвано алгоритмом использования переменных в программах. Например, мы можем увеличить значение переменной и затем, основываясь на полученном значении, принять решение о дальнейшем порядке выполнения операций. Совокупность операций, состоящая из увеличения значения переменной и проверки полученного значения, не является атомарной, и таким образом появляется вероятность принятия неверного решения.
Основными средствами синхронизации являются: взаимные исключения (mutual exclusions – mutex), условные переменные (conditional variables) и блокировки чтения-записи.
Взаимные исключения и условные переменные появились в стандарте Posix.1 для программных потоков, и всегда могут быть использованы для синхронизации отдельных потоков одного процесса. Стандарт Posix.1 также разрешает использовать взаимное исключение или условную переменную и для синхронизации нескольких процессов, если это исключение или переменная хранится в области памяти, совместно используемой процессами.
В этой главе мы разберем классическую схему производитель-потребитель, используя взаимные исключения и условные переменные. В примере будут использоваться программные потоки, а не процессы, поскольку предоставить потокам общий буфер данных, предполагаемый в этой задаче, легко, а вот создать буфер данных между процессами можно только с помощью одной из форм разделяемой памяти, описываемой в разделе 4.12. Еще одно решение этой задачи, уже с использованием семафоров, появится вглаве 10.
