Скачиваний:
90
Добавлен:
12.05.2015
Размер:
913.92 Кб
Скачать

15.2.1. Схема производитель-потребитель

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

С этой задачей мы регулярно сталкиваемся при использовании каналов Unix. Команда интерпретатора, использующая канал

grep pattern chapters.* | wc –l

является примером такой задачи. Программа grepвыступает как единственный производитель, аwc– как единственный потребитель. Канал используется как форма IPC. Требуемая синхронизация между производителем и потребителем обеспечивается ядром, обрабатывающим командыwriteпроизводителя иreadпотребителя. Если производитель опережает потребителя (канал переполняется), ядро приостанавливает производителя при вызовеwrite, пока в канале не появится место. Если потребитель опережает производителя (канал опустошается), ядро приостанавливает потребителя при вызовеread, пока в канале не появятся данные.

Такой тип синхронизации называется неявным; производитель и потребитель не знают о том, что синхронизация вообще осуществляется. Если бы мы использовали очередь сообщений System V в качестве средства IPC между производителем и потребителем, ядро снова взяло бы на себя обеспечение синхронизации.

При использовании разделяемой памяти как средства IPC между производителем и потребителем, однако, требуется использование какого-либо вида явной синхронизации. Мы продемонстрируем это, воспользовавшись взаимным исключением. Схема рассматриваемого примера изображена на рис. 15.4.

рис. 15.4

В одном процессе у нас имеется несколько потоков-производителей и один поток-потребитель. Целочисленный массив buffсодержит производимые и потребляемые данные (данные совместного пользования). Для простоты производители просто устанавливают значенияbuff[0]в 0,buff[1]в 1 и т. д. Потребитель перебирает элементы массива, проверяя правильность записей.

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

Листинг 15.1. Пример системы производитель-потребитель

#include <pthread.h>

#include <stdio.h>

#include <stdlib.h>

#define min(a,b) ((a) < (b) ? (a) : (b))

#define MAXNITEMS 1000000

#define MAXNTHREADS 100

int res;

int nitems; /* Только для чтения производителем и потребителем */

struct

{

pthread_mutex_t mutex;

int buff[MAXNITEMS];

int nput;

int nval;

} shared = { PTHREAD_MUTEX_INITIALIZER };

void *produce(void *arg)

{

for ( ; ; )

{

if ((res = pthread_mutex_lock(&shared.mutex)) != 0)

{

fprintf(stderr, "Ошибка запирания взаимного исключения: %s\n",

strerror(res));

exit(1);

}

if (shared.nput >= nitems)

{ /* массив заполнен, работа окончена */

if ((res = pthread_mutex_unlock(&shared.mutex)) != 0)

{

fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",

strerror(res));

exit(1);

}

return(NULL);

}

shared.buff[shared.nput] = shared.nval;

shared.nput++;

shared.nval++;

if ((res = pthread_mutex_unlock(&shared.mutex)) != 0)

{

fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",

strerror(res));

exit(1);

}

*((int *) arg) += 1;

}

}

void *consume(void *arg)

{

int i;

for (i = 0; i < nitems; i++)

if (shared.buff[i] != i)

printf("buff[%d] = %d\n", i, shared.buff[i]);

return(NULL);

}

int main(int argc, char *argv[])

{

int i, nthreads, count[MAXNTHREADS];

pthread_t tid_produce[MAXNTHREADS], tid_consume;

if (argc < 3)

{

printf("Использование: %s <число_элементов> <число_потоков>\n", argv[0]);

exit(-1);

}

nitems = min(atoi(argv[1]), MAXNITEMS);

nthreads = min(atoi(argv[2]), MAXNTHREADS);

if ((res = pthread_setconcurrency(nthreads)) != 0)

{

fprintf(stderr, "Ошибка при вызове функции pthread_setconcurrency: %s\n", strerror(res));

exit(1);

}

for (i = 0; i < nthreads; i++)

{ /* Запуск всех потоков-производителей */

count[i] = 0;

if ((res = pthread_create(&tid_produce[i], NULL, produce, &count[i])) != 0)

{

fprintf(stderr, "Ошибка создания потока-производителя номер %d: %s\n",

i, strerror(res));

exit(1);

}

}

for (i = 0; i < nthreads; i++)

{ /* Ожидание завершения всех потоков-производителей */

if ((res = pthread_join(tid_produce[i], NULL)) != 0)

{

fprintf(stderr, "Ошибка завершения потока-производителя номер %d: %s\n",

i, strerror(res));

exit(1);

}

printf("count[%d] = %d\n", i, count[i]);

} /* Запуск и ожидание завершения потока-потребителя */

if ((res = pthread_create(&tid_consume, NULL, consume, NULL)) != 0)

{

fprintf(stderr, "Ошибка создания потока-потребителя: %s\n", strerror(res));

exit(1);

}

if ((res = pthread_join(tid_consume, NULL)) != 0)

{

fprintf(stderr, "Ошибка завершения потока-потребителя: %s\n",

strerror(res));

exit(1);

}

exit(0);

}

Переменные, которые совместно используются потоками, объединены в структуру с именем sharedвместе с взаимным исключением, чтобы подчеркнуть, что доступ к переменным возможен только при условии запирания взаимного исключения. Переменнаяnputхранит индекс следующего элемента массиваbuff, подлежащего обработке, а переменнаяnvalсодержит следующее значение, которое должно быть помещено в массив (0, 1, 2 и т. д.). Мы выделяем память под эту структуру и статически инициализируем взаимное исключение, используемое для синхронизации действий потоков-производителей.

Формирование данных происходит внутри функции produce. Критическая область кода производителя состоит из проверки на достижение конца массива (завершение работы)

if (shared.nput >= nitems)

и трех строк, помещающих очередное значение в массив:

shared.buff[shared.nput] = shared.nval;

shared.nput++;

shared.nval++;

Мы защищаем эту область с помощью взаимного исключения, не забывая отпереть его после завершения работы. Обратите внимание, что увеличение элемента массива count(через указательarg) не относится к критической области, поскольку каждый поток имеет свой личный счетчик. Поэтому мы не включаем эту строку в критическую область, защищаемую взаимным исключением. Один из принципов хорошего стиля программирования заключается в минимизации объема кода, защищаемого взаимным исключением.

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

Первый аргумент командной строки указывает количество элементов, которые будут произведены производителями, а второй – количество запускаемых потоков-производителей, каждый из которых вызывает функцию produce. Идентификаторы потоков хранятся в массивеtid_produce. Аргументом каждого потока-производителя является адрес соответствующего элемента массиваcount. Счетчики инициализируют нулевым значением, и каждый поток увеличивает значение своего счетчика на 1 после помещения очередного элемента в буфер. Содержимое массива счетчиков затем выводится на экран, так что мы можем узнать, сколько элементов было помещено в буфер каждым из потоков.

Мы ожидаем завершения работы всех потоков-производителей, выводя содержимое счетчика для каждого потока, а затем запускаем единственный поток-потребитель. Таким образом (на данный момент) мы исключаем необходимость синхронизации между потребителем и производителями. Мы ждем завершения работы потока-потребителя, а затем завершаем работу всего процесса.

При запуске только что описанной программы с пятью потоками-производителями, которые должны вместе создать один миллион элементов данных, мы получим следующий результат:

$ ./a.out 1000000 5

count[0] = 41008

count[1] = 194203

count[2] = 181203

count[3] = 88766

count[4] = 494820

Если убрать из этого примера установку блокировки с помощью взаимного исключения, то он будет работать неправильно, как и предполагается. Потребитель обнаружит множество элементов buff[i], значения которых будут отличны отi. Также можно убедиться, что удаление блокировки ничего не изменит, если запустить на выполнение только один поток-производитель.

Соседние файлы в папке Chapter.4