- •Глава 8. Параллельное выполнение процессов
- •8.1. Постановка проблемы
- •8.2. Взаимное исключение запретом прерываний
- •8.3. Взаимное исключение через общие переменные
- •Вариант 2: переменная-переключатель
- •Алгоритм Деккера
- •Алгоритм Питерсона
- •8.4. Команда testAndSet и блокировки
- •Xchg al,lock
- •8.5. Семафоры
- •8.6. "Производители–потребители"
- •8.7. Конструкции критических секций в языках программирования
- •8.8. Мониторы
- •8.9. "Читатели–писатели" и групповые мониторы
- •8.10. Примитивы синхронизации в языках программирования
- •8.11. Рандеву
- •Контрольные вопросы
8.6. "Производители–потребители"
Пусть мы имеем два циклических процесса, которые мы назовем "производитель" и "потребитель". Производитель в каждой итерации своего цикла вырабатывает (производит) порцию информации, которую он помещает в общий для обоих процессов буфер. Предположим для начала, что емкость буфера неограничена. Потребитель в каждой итерации своего цикла выбирает из буфера порцию информации, выработанную производителем, и обрабатывает (потребляет) ее. Потребитель не должен начинать обработку порции, пока ее производство не будет закончено. Задача состоит в синхронизации действий производителя и потребителя таким образом, чтобы не допустить потерь и искажений информации, во-первых, и голодания процессов, во-вторых.
Решение достигается при помощи единственного общего семафора, играющего роль счетчика числа порций в буфере (здесь и далее мы предполагаем структуру семафора, определенную в предыдущем разделе):
1 2 3 4 5 6 7 8 9 10 11 12 12 14 15 16 17 18 19 |
static semaphore *portCnt = { 0, 0, NULL }; static ... buffer ...; /* процесс-производитель */ void producer ( void ) { while (1) { < производство порции > < добавление порции в буфер > V(portCnt); } } /* процесс-потребитель */ void consumer ( void ) { while (1) { P(portCnt) < выборка порции из буфера > < обработка порции > } } |
Исходное значение семафора portCnt – 0. Производитель каждую итерацию своего цикла заканчивает V-операцией, увеличивающей значение счетчика. Потребитель каждую свою итерацию начинает P-операцией. Если буфер пуст, то потребитель задержится в своей P-операции до появления в буфере очередной порции. Таким образом, если потребитель работает быстрее производителя, он будет время от времени простаивать, если производитель работает быстрее – в буфере будут накапливаться порции.
В реальных задачах такого рода (например, кольцевой буферизации, рассмотренной нами в главе 6) буфер всегда имеет некоторую конечную емкость. Если производитель работает быстрее, то при заполнении буфера он должен приостанавливаться, ожидая освобождения места в буфере. Это легко обеспечить, введя новый семафор freeCnt, выполняющий роль счетчика свободных мест в буфере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
static semaphore *portCnt = { 0, 0, NULL }, *freeCnt = { BSIZE, 0, NULL }, static ... buffer [BSIZE]; /* процесс-производитель */ void producer ( void ) { while (1) { < производство порции > P(freeCnt); < добавление порции в буфер > V(portCnt); } } /* процесс-потребитель */ void consumer ( void ) { while (1) { P(portCnt) < выборка порции из буфера > V(freeCnt); < обработка порции > } } |
Попытаемся теперь обобщить решение для произвольного числа производителей и потребителей. Но в таком обобщении мы сталкиваемся с еще одной существенной особенностью. В приведенных выше решениях мы допускали одновременное выполнение операций <добавление порции в буфер> и <выборка порции из буфера>. Очевидно, что при любой организации буфера производитель и потребитель могут одновременно работать только с разными порциями информации. Иначе обстоит дело с многочисленными производителями и потребителями. Два производителя могут попытаться одновременно добавить порцию в буфер и выбрать для этого одно и то же место в буфере. Аналогично два потребителя могут попытаться выбрать из буфера одну и ту же порцию. В следующем примере мы для наглядности представляем буфер в виде кольцевой очереди с элементами (порциями) одинакового размера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
typedef ... portion; /* порция информации */ static portion buffer [BSIZE]; static int wIndex = 0, rIndex = 0; static semaphore *portCnt = { 0, 0, NULL }, *freeCnt = { BSIZE, 0, NULL }, *rAccess = { 1, 0, NULL }, *wAccess = { 1, 0, NULL }; /* имеется NP аналогичных процессов-производителей */ void producer ( void ) { portion work; while (1) { < производство порции в work > P(wAccess); P(freeCnt); /* добавление порции в буфер */ memcpy(buffer+wIndex,&work, sizeof(portion) ); if ( ++wIndex == BSIZE ) w_index = 0; V(portCnt); V(wAccess); } } /* имеется NC аналогичных процессов-потребителей */ void consumer ( void ) { portion work; while (1) { P(rAccess); P(portCnt) /* выборка порции из буфера */ memcpy(&work, buffer+rIndex, sizeof(portion) ); if ( ++rIndex == BSIZE ) rIndex = 0; V(freeCnt); V(rAaccess); < обработка порции в work> } } |
Мы оформляем обращения к буферу как критические секции, защищая их семафорами rAccess и wAccess. Поскольку конфликтовать (пытаться работать с одной и той же порцией в буфере) могут только однотипные процессы, мы рассматриваем буфер как два ресурса: ресурс для чтения и ресурс для записи, и каждый такой ресурс защищается своим семафором. Таким образом, запрещается одновременный доступ к буферу двух производителей или двух потребителей, но разрешается одновременный доступ одного производителя и одного потребителя.