Литмир - Электронная Библиотека
Содержание  
A
A

Задайте

run_now
равной 1, когда выполняется функция
main
, и 2, когда выполняется новый поток.

В функцию

main
после создания нового потока добавьте следующий код:

int print_count1 = 0;

while (print_count1+ < 20) {

 if (run_now == 1) {

  printf("1");

  run_now = 2;

 } else {

  sleep(1);

 }

}

Если переменная

run_now
равна 1, выведите "1" и присвойте переменной значение 2. В противном случае вы на короткое время засыпаете и снова проверяете значение. Вы ждете, пока значение изменится на 1, проверяя время от времени снова. Этот прием называется циклам активного или деятельного ожидания (busy wait), несмотря, на то, что в данном случае программа засыпает на секунду между очередными проверками. Позже в этой главе вы увидите, как сделать это лучше.

В функции

thread_function
, где выполняется ваш новый поток, вы делаете примерно то же самое, но с противоположными значениями.

int print_count2 = 0;

while (print_count2++ < 20) {

 if (run_now == 2) {

  printf("2");

  run_now = 1;

 } else {

  sleep(1);

 }

}

Вы удаляете переданные параметр и возвращаемое значение, т.к. они вас больше не интересуют.

Когда вы выполните программу, то увидите следующий вывод. (Вы можете обнаружить, что для формирования вывода, особенно на машине с одноядерным ЦП, программе потребуется несколько секунд.)

$ <b>cc -D_REENTRANT thread2.с -о thread2 -lpthread</b>

$ <b>./thread2</b>

12121212121212121212

Waiting for thread to finish...

Thread joined

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

Каждый поток заставляет другой поток выполняться, задавая переменную

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

Синхронизация

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

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

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

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

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

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

Имена функций семафоров начинаются не с префикса

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

Семафор создается с помощью функции

sem_init
, которая объявляется следующим образом.

<b>#include &lt;semaphore.h&gt;</b>

<b>int sem_init(sem_t *sem, int pshared, unsigned int value);</b>

Эта функция инициализирует объект-семафор, на который указывает параметр

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

218
{"b":"285844","o":1}