Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 46 (всего у книги 67 страниц)
Синхронизация
В предыдущем разделе вы видели, что два потока выполняются одновременно, но метод переключения между ними топорный и очень неэффективный. К счастью, существует ряд функций, специально разработанных для предоставления лучших способов управления исполнением потоков и доступа к важным фрагментам кода.
В этом разделе мы рассмотрим два основных метода: семафоры, действующие как сторожа, охраняющие фрагменты кода, и мьютексы или исключающие семафоры, действующие как устройство взаимного исключения (отсюда и имя – исключающий семафор) для защиты фрагментов программного кода. На самом деле эти методы похожи, и один может быть описан в терминах другого. Тем не менее существуют ситуации, в которых семантика проблемы делает один более выразительным, чем другой. Например, управление доступом к некоторой области совместно используемой памяти, к которой может обращаться только один поток в каждый момент времени, более естественным кажется исключающий семафор или мьютекс. Для управления доступом к ряду идентичных объектов в целом, например, предоставление потоку одной телефонной линии из набора, включающего пять доступных линий, больше подходит семафор. Какой метод выберите вы, зависит от личных предпочтений и наиболее подходящего для вашей программы алгоритма.
Синхронизация с помощью семафоровДля семафоров есть два набора интерфейсных функций: один взят из POSIX Realtime Extensions (дополнения POSIX для режима реального времени) и применяется для потоков, а другой, известный как семафоры System V, обычно применяется для синхронизации процессов. (Мы обсудим второй тип в главе 14.) Оба набора не гарантируют взаимозаменяемости и хотя очень похожи, используют вызовы разных функций.
Дейкстра, голландский ученый, специалист по компьютерным наукам, первым сформулировал идею семафоров. Семафор – это переменная особого типа, которая может изменяться с положительным или отрицательным приращением, но обращение к переменной в ответственный момент всегда атомарно даже в многопоточных программах. Это означает, что если два потока (или несколько) в программе пытаются изменить значение семафора, система гарантирует, что все операции будут на самом деле выполняться одна за другой. В случае обычных переменных результат конфликтных операций разных потоков в одной программе произволен.
В этом разделе мы рассмотрим простейший тип семафора, двоичный или бинарный семафор, который принимает только значения 0 и 1. Существует и более обобщенный вид семафора, считающий (counting) семафор, принимающий более широкий диапазон значений. Обычно семафоры используются для защиты фрагмента программного кода, так чтобы только один поток исполнения мог изменить его в любой конкретный момент времени. Для этого нужен двоичный семафор. Порой вам необходимо разрешить ограниченному числу потоков выполнять заданный фрагмент кода, для этого вам следует применять считающий семафор. Поскольку считающие семафоры гораздо менее популярны, мы не будем их обсуждать в дальнейшем, отметив лишь, что они представляют собой логическое расширение двоичного семафора и что реальные вызовы функций должны быть идентичны.
Имена функций семафоров начинаются не с префикса pthread_
, как большинство функций, относящихся к потокам, а с sem_
. Для работы с потоками применяют четыре базовые функций семафоров. Они все очень просты.
Семафор создается с помощью функции sem_init
, которая объявляется следующим образом.
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
Эта функция инициализирует объект-семафор, на который указывает параметр sem
, задает вариант его совместного использования (который мы обсудим через минуту) и присваивает ему начальное целочисленное значение. Параметр pshared
управляет типом семафора. Если pshared
равен 0, семафор локален по отношению к текущему процессу. В противном случае семафор может быть совместно использован разными процессами. Нас сейчас интересуют семафоры, которые не используются совместно разными процессами. Во время написания книги ОС Linux не поддерживала такое совместное использование и передача ненулевого значения параметру pshared
приводила к аварийному завершению вызова.
Следующая пара функций управляет значением семафора и объявляется следующим образом.
#include
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
int sem_destroy(gem_t* sem);
И снова эта функция принимает указатель на семафор и очищает любые ресурсы, которые у него могли быть. Если вы попытаетесь уничтожить семафор, которого дожидается какой-либо поток, то получите ошибку.
Как и большинство других, функций, все перечисленные функции возвращают 0 в случае успешного завершения.
А теперь выполните упражнение 12.3.
Упражнение 12.3. Семафор потока
Текст этой программы thread3.c также основан на тексте программы thread1.c. Поскольку изменения значительны, мы приводим новый вариант полностью.
#include
#include
#include
#include
#include
#include
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
ждать, пока у считающего потока не появится возможность закончить свой подсчет, но гораздо легче применить мьютекс или исключающий семафор, который мы рассмотрим далее.
Другой способ синхронизации доступа в многопоточных программах – применение мьютексов (сокращение от mutual exclusions – взаимные исключения) или исключающих семафоров, которые разрешают программистам «запирать» объект так, что только один поток может обратиться к нему.
Базовые функции, необходимые для использования мьютексов, очень похожи на функции семафоров. Они объявляются следующим образом:
#include <рthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex,
const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread mutex_t* mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Как обычно, в случае успешного завершения возвращается 0 и код ошибки в случае аварийного завершения, но переменная errno
не задается, вам придется использовать код возврата.
Как и функции семафоров, функции мьютексов принимают указатель на предварительно объявленный объект, в данном случае типа pthread_mutex_t
. Дополнительный параметр атрибутов в функции pthread_mutex_init
позволяет задать атрибуты мьютекса, управляющие его поведением. По умолчанию тип атрибута – «fast». У него есть небольшой недостаток: если ваша программа попытается вызвать функцию pthread_mutex_lock
для мьютекса, который уже заблокирован, программа блокируется. Поскольку поток, удерживающий блокировку, в данный момент заблокирован, мьютекс никогда не будет открыт, и программа попадает в тупиковую ситуацию. Есть возможность изменить атрибуты мьютекса так, чтобы он либо проверял наличие такой ситуации и возвращал ошибку, либо действовал рекурсивно и разрешал множественные блокировки тем же самым потоком, если будет такое же количество разблокировок в дальнейшем.
Установка атрибутов мьютекса в этой книге не рассматривается, поэтому мы будем передавать NULL
в указателе на атрибуты, и использовать поведение по умолчанию. Дополнительную информацию об изменении атрибутов можно найти в интерактивном справочном руководстве к функции pthread_mutex_init
.
Выполните упражнение 12.4.
Упражнение 12.4. Мьютекс потока
Далее приводится еще одна модификация исходной программы thread1.с, но значительно измененная. На этот раз вы уделите особое внимание доступу к вашим важным переменным и примените мьютекс для того, чтобы быть уверенными в том, что они доступны в любой момент времени только одному потоку. Для легкости чтения текста примера мы пропустили некоторые проверки ошибок при возвратах из мьютекса, заблокированного и открытого. В рабочем программном коде вы обязательно должны проверять эти возвращаемые значения. Далее приведен текст новой программы thread4.c.
#include
#include
#include
#include
#include
#include
void *thread_function(void *arg);
pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
int main() {
int res;
pthread_t a_thread;
void *thread_result;
res = pthread_mutex_init(&work_mutex, NULL);
if (res != 0) {
perror(«Mutex initialization failed»);
exit(EXIT_FAILURE);
}
res pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0) {
perror(«Thread creation failed»);
exit(EXIT_FAILURE);
}
pthread_mutex_lock(&work_mutex);
printf(«Input same text. Enter 'end' to finishn»);
while (!time_to_exit) {
fgets (work_area, WORK_SIZE, stdin);
pthread_mutex_unlock(&work_mutex);
while(1) {
pthread_mutex_lock(&work_mutex);
if (work_area[0] != ' ') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
} else {
break;
}
}
}
pthread_mutex_unlock(&work_mutex);
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»);
pthread_mutex_destroy(&work_mutex);
exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
sleep(1);
pthread_mutex_lock(&work_mutex);
while(strncmp(«end», work_area, 3) ! = 0) {
printf(«You input %d charactersn», strlen(work_area)-1);
work_area[0] = ' ';
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
while (work_area[0] == ' ') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
}
}
time_to_exit = 1;
work_area[0] = ' ';
pthread_mutex_unlock(&work_mutex);
pthread_exit(0);
}
После запуска вы получите следующий вывод:
$ cc -D_REENTRANT thread4.с -о thread4 -lpthread
$ ./thread4
Input some text. Enter 'end' to finish
Whit
You input 4 characters
The Crow Road
You input 13 characters
end
Waiting for thread to finish...
Thread joined
Как это работает
Вы начинаете с объявления мьютекса вашей рабочей области и на сей раз дополнительной переменной time_to_exit
:
pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
Далее инициализируется мьютекс:
res = pthread_mutex_init(&work_mutex, NULL);
if (res != 0) {
perror(«Mutex initialization failed»);
exit(EXIT_FAILURE);
}
Затем запускается новый поток. Далее приведен код, выполняемый в функции потока:
pthread_mutex_lock(&work_mutex);
while(strncmp(«end», work_area, 3) != 0) {
printf(«You input id charactersn», strlen(work_area)-1);
work_area[0] = ' ';
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
while (work_area[0] == ' ') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
}
}
time_to_exit = 1;
work_area[0] = ' ';
pthread_mutex_unlock(&work_mutex);
Сначала новый поток пытается заблокировать мьютекс. Если он уже заблокирован, вызов задерживается до тех пор, пока мьютекс не освободится. После получения доступа вы проверяете, нет ли к вам запроса на завершение выполнения. Если запрашивается завершение, просто задайте переменную time_to_exit
, сотрите первый символ в рабочей области и завершите выполнение.
Если вы не хотите завершать выполнение, сосчитайте символы и очистите первый символ, сделав его пустым (null). Пустой первый символ применяется как способ информирования считывающей программы о завершении подсчета символов. Далее вы открываете мьютекс и ждете выполнения потока main
. Периодически вы пытаетесь заблокировать мьютекс и, когда вам это удается, проверяете, подготовил ли поток main новую работу для вас. Если нет, вы открываете мьютекс и ждете какое-то время. Если работа есть, вы считаете символы и выполняете проход цикла снова.
Далее приведен поток main
.
pthread_mutex_lock(&work_mutex)
printf(«Input some text. Enter 'end' to finishn»);
while (!time_to_exit) {
fgets(work_area, WORK_SIZE, stdin);
pthread_mutex_unlock(&work_mutex);
while(1) {
pthread_mutex_lock(&work_mutex);
if (work_area[0] != ' ') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
} else {
break;
}
}
}
pthread_mutex_unlock(&work_mutex);
Он аналогичен второму потоку. Вы блокируете рабочую область и можете читать в нее текст, а затем вы снимаете блокировку, чтобы открыть доступ другому потоку для подсчета слов. Периодически вы блокируете мьютекс, проверяете, сосчитаны ли слова (элемент work_area[0]
равен пустому символу), и освобождаете мьютекс, если нужно продолжить ожидание. Как уже отмечалось ранее, этот вид опроса и получения ответа в основном не слишком удачный прием и в реальной жизни вам, возможно, придется применить семафор для его замены. Тем не менее, программный код справляется с задачей демонстрации примера применения мьютекса.