Книга: Основы программирования в Linux

Синхронизация с помощью семафоров

Синхронизация с помощью семафоров

Для семафоров есть два набора интерфейсных функций: один взят из POSIX Realtime Extensions (дополнения POSIX для режима реального времени) и применяется для потоков, а другой, известный как семафоры System V, обычно применяется для синхронизации процессов. (Мы обсудим второй тип в главе 14.) Оба набора не гарантируют взаимозаменяемости и хотя очень похожи, используют вызовы разных функций.

Дейкстра, голландский ученый, специалист по компьютерным наукам, первым сформулировал идею семафоров. Семафор — это переменная особого типа, которая может изменяться с положительным или отрицательным приращением, но обращение к переменной в ответственный момент всегда атомарно даже в многопоточных программах. Это означает, что если два потока (или несколько) в программе пытаются изменить значение семафора, система гарантирует, что все операции будут на самом деле выполняться одна за другой. В случае обычных переменных результат конфликтных операций разных потоков в одной программе произволен.

В этом разделе мы рассмотрим простейший тип семафора, двоичный или бинарный семафор, который принимает только значения 0 и 1. Существует и более обобщенный вид семафора, считающий (counting) семафор, принимающий более широкий диапазон значений. Обычно семафоры используются для защиты фрагмента программного кода, так чтобы только один поток исполнения мог изменить его в любой конкретный момент времени. Для этого нужен двоичный семафор. Порой вам необходимо разрешить ограниченному числу потоков выполнять заданный фрагмент кода, для этого вам следует применять считающий семафор. Поскольку считающие семафоры гораздо менее популярны, мы не будем их обсуждать в дальнейшем, отметив лишь, что они представляют собой логическое расширение двоичного семафора и что реальные вызовы функций должны быть идентичны.

Имена функций семафоров начинаются не с префикса pthread_, как большинство функций, относящихся к потокам, а с sem_. Для работы с потоками применяют четыре базовые функций семафоров. Они все очень просты.

Семафор создается с помощью функции sem_init, которая объявляется следующим образом.

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

Эта функция инициализирует объект-семафор, на который указывает параметр sem, задает вариант его совместного использования (который мы обсудим через минуту) и присваивает ему начальное целочисленное значение. Параметр pshared управляет типом семафора. Если pshared равен 0, семафор локален по отношению к текущему процессу. В противном случае семафор может быть совместно использован разными процессами. Нас сейчас интересуют семафоры, которые не используются совместно разными процессами. Во время написания книги ОС Linux не поддерживала такое совместное использование и передача ненулевого значения параметру pshared приводила к аварийному завершению вызова.

Следующая пара функций управляет значением семафора и объявляется следующим образом.

#include <semaphore.h>
int sem_wait(sem_t* sem);
int sem_post(sem_t* sem);

Обе они принимают указатель на объект-семафор, инициализированный вызовом sem_init.

Функция sem_post атомарно увеличивает значение семафора на 1. Атомарно в данном случае означает, что если два потока одновременно пытаются увеличить значение единственного семафора на 1, они не мешают друг другу, как в случае двух программ, которые читают, увеличивают и записывают значение в файл в одно и то же время. Если обе программы пытаются увеличить значение на 1, семафор всегда будет корректно увеличивать значение на 2.

Функция sem_wait атомарно уменьшает значение семафора на единицу, но всегда ждет до тех пор, пока сначала счетчик семафора не получит ненулевое значение. Таким образом, если вы вызываете sem_wait для семафора со значением 2, поток продолжит выполнение, а семафор будет уменьшен до 1. Если sem_wait вызывается для семафора со значением 0, функция будет ждать до тех пор, пока какой-нибудь другой поток не увеличит значение, и оно станет ненулевым. Если оба потока ждут в функции sem_wait, чтобы один и тот же семафор стал ненулевым, и он увеличивается когда-нибудь третьим потоком, только один из двух ждущих потоков получит возможность уменьшить семафор и продолжиться; другой поток так и останется ждущим. Эта атомарная способность "проверить и установить" в одной функции и делает семафор столь ценным.

Примечание

Есть и другая функция семафора sem_trywait — это неблокирующий партнер sem_wait. Мы не будем ее обсуждать в книге в дальнейшем, дополнительную информацию см. в интерактивном справочном руководстве.

Последняя функция семафоров — sem_destroy. Она очищает семафор, когда вы закончили работу с ним, и объявляется следующим образом:

#include <semaphore.h>
int sem_destroy(gem_t* sem);

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

Как и большинство других, функций, все перечисленные функции возвращают 0 в случае успешного завершения.

А теперь выполните упражнение 12.3.

Упражнение 12.3. Семафор потока

Текст этой программы thread3.c также основан на тексте программы thread1.c. Поскольку изменения значительны, мы приводим новый вариант полностью.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_function(void *arg);
sem_t bin_sem;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int main() {
 int res;
 pthread_t a_thread;
 void *thread result;
 res = sem_init(&bin_sem, 0, 0);
 if (res != 0) {
  perror("Semaphore initialization failed");
  exit(EXIT_FAILURE);
 }
 res = pthread_create(&a_thread, NULL, thread_function, NULL);
 if (res != 0) {
  perror("Thread creation failed");
  exit(EXIT_FAILURE);
 }
 printf("Input some text. Enter 'end' to finishn");
 while (strncmp("end", work_area, 3) != 0) {
  fgets(work_area, WORK_SIZE, stdin);
  sem_post(&bin_sem);
 }
 printf("nWaiting for thread to finish...n");
 res = pthread_join(a_thread, &thread_result);
 if (res != 0) {
  perror("Thread join failed");
  exit(EXIT_FAILURE);
 }
 printf("Thread joinedn");
 sem_destroy(&bin_sem);
 exit(EXIT_SUCCESS);
}
void *thread function(void *arg) { sem_wait(&bin_sem);
 while(strncmp("end", work area, 3) != 0) {
  printf("You input %d charactersn", strlen(work_area)-1);
  sem_wait(&bin_sem);
 }
 pthread_exit(NULL);
}

Первое важное изменение — включение файла semaphore.h для обеспечения доступа к функциям семафоров. Далее вы объявляете семафор и несколько переменных и инициализируете семафор перед тем, как создать новый поток.

sem_t bin_sem;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int main() {
 int res;
 pthread_t a_thread;
 void *thread_result;
 res = sem_init(&bin_sem, 0, 0);
 if (res != 0) {
  perror("Semaphore initialization failed");
  exit(EXIT_FAILURE);
 }

Обратите внимание на то, что начальное значение семафора равно 0.

В функции main, после того как вы запустили новый поток, вы читаете некоторый текст с клавиатуры, загружаете вашу рабочую область и затем наращиваете счетчик семафора с помощью sem_post:

 printf("Input some text. Enter 'end' to finishn");
 while(strncmp("end", work_area, 3) != 0) {
  fgets(work_area, WORK_SIZE, stdin);
  sem_post(&bin_sem);
 }

В новом потоке вы ждете семафор и затем подсчитываете символы ввода:

 sem_wait(&bin_sem);
 while(strncmp("end", work_area, 3) != 0) {
  printf("You input %d charactersn", strlen(work_area)-1);
  sem_wait(&bin_sem);
 }

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

И опять потоки совместно используют один и тот же массив work_area. Для того чтобы программный код был короче и за ним легче было следить, мы опять пропустили некоторые проверки ошибок, например значения, возвращаемые из функции sem_wait. Но в рабочем программном коде вы всегда должны проверять ошибочные возвращаемые значения, если нет достаточных оснований для отказа от проверки.

Дайте программе отработать:

$ cc -D_REENTRANT thread3.с -о threads -lpthread
$ ./thread3
Input some text. Enter 'end', to finish
The Wasp Factory
You input 16 characters
Iain Banks
You input 10 characters
end
Waiting for thread to finish...
Thread joined

В программах с потоками временные ошибки всегда трудно найти, но программа кажется приспособленной и к быстрому вводу текста, и более неспешным паузам.

Как это работает

Когда вы инициализируете семафор, то задаете ему начальное значение, равное 0. Следовательно, когда запускается функция потока, вызов sem_wait приостанавливает выполнение и ждет, когда семафор станет ненулевым.

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

Неочевидные недочеты в разработке, которые заканчиваются в результате неявными ошибками, легко пропустить. Давайте слегка изменим программу на thread3a.c, так чтобы вводимый с клавиатуры текст временами заменялся автоматически формируемым текстом. Замените цикл чтения в main следующим:

printf("Input some text. Enter 'end' to finishn");
while (strncmp("end", work_area, 3) != 0) {
 if (strncmp(work_area, "FAST", 4) == 0) {
  sem_post(&bin_sem);
  strcpy(work_area, "Wheeee...");
 } else {
  fgets(work_area, WORK_SIZE, stdin);
 }
 sem_post(&bin_sem);
}

Теперь, если вы введете FAST, программа вызовет sem_post, чтобы запустить счетчик символов, но немедленно обновит work_area чем-то другим.

$ cc -D_REENTRANT thread3a.с -о thread3a -lpthread
$ ./thread3a
Input some text. Enter 'end' to finish
Excession
You input 9 characters
FAST
You input 7 characters
You input 7 characters
You input 7 characters
end
Waiting for thread to finish...
Thread joined

Проблема этой программы заключается в том, что она рассчитывала на то, что ввод текста из программы продлится так долго, что у другого потока хватит времени для подсчета символов до того, как поток main подготовится передать ему новую порцию текста для подсчета. Когда вы попытались предложить ему два набора слов для подсчета, быстро следующих друг за другом (FAST с клавиатуры и затем Wheeee..., формируемое автоматически), у второго потока не было времени для выполнения. Но семафор наращивался несколько раз, поэтому считающий поток продолжал считать слова и уменьшал значение семафора до тех пор, пока оно снова не стало нулевым.

Этот пример показывает, как аккуратны вы должны быть с временны?ми условиями в многопоточных программах. Исправить программу можно, применяя дополнительный семафор для того, чтобы заставить поток main ждать, пока у считающего потока не появится возможность закончить свой подсчет, но гораздо легче применить мьютекс или исключающий семафор, который мы рассмотрим далее.

Оглавление книги


Генерация: 0.831. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз