- •Глава 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.10. Примитивы синхронизации в языках программирования
До сих пор мы рассматривали методы, которые обеспечивают и взаимное исключение, и синхронизацию. Целый ряд прикладных задач, однако, требует только синхронизации без взаимного исключения. Отказ от последнего, если это не мешает решению задачи, всегда оправдан, так как взаимное исключение усложняет решение и может снижать уровень мультипрограммирования.
Один из возможных примитивов, обеспечивающих синхронизацию без взаимного исключения, называется счетчиком событий. Счетчик событий – тип данных, представляемый неуменьшающимся целым числом с начальным значением 0. Его значение в любой момент времени – число событий определенного типа, происшедших от некоторой точки начала отсчета. Над этим типом данных возможны следующие операции:
advance(E) – увеличение значения счетчика событий E на 1, атомарная операция;
eread(E) – возвращает текущее значение счетчика E, эта операция не взаимоисключающая с advance, так что к моменту, когда значение попадет в читающий его процесс, текущее значение счетчика может быть уже изменено;
await(E,value) – ждать – ожидание (блокировка процесса), пока значение счетчика E не станет большим, чем value, или равным ему.
Существенно, что из перечисленных операций только advance является взаимоисключающей, остальные могут выполняться параллельно друг с другом и с advance.
Вот как решается с помощью счетчиков событий задача для одного производителя и одного потребителя:
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 40 41 42 43 44 |
/* тип данных - счетчик событий */ typedef unsigned int eventcounter /* счетчики для чтения и записи */ static eventcounter inCnt = 0, outCnt = 0; /* буфер */ static portion buffer [BUFSIZE]; /* процесс-потребитель */ void consumer ( void ) { int portNum; /* номер порции */ /* рабочая область порции */ portion work; /* цикл потребления */ for ( portNum = 1; ; portNum++ ) { /* ожидание доступности порции по номеру */ await (inCnt, portNum); /* выборка из буфера */ memcpy (&work, buffer + portNum % BSIZE, sizeof(portion) ); /* продвижение счетчика записи */ advance (outCnt); < обработка порции в work> } } /* процесс-производитель */ void producer ( void ) { int portNum; /* номер порции */ /* рабочая область для порции */ portion work; /* цикл производства */ for ( portNum = 1; ; portNum++ ) { < производство порции в work > /* ожидание доступности порции по номеру */ await (outCnt, portNum - BSIZE); /* запись в буфер */ memcpy (buffer + portNum % BSIZE, &work, sizeof(portion) ); /* продвижение счетчика чтения */ advance (inCnt); } } |
Как мы уже отмечали выше, производитель и потребитель работают с разными секциями буфера и взаимное исключение для них не требуется. Процессы – производитель и потребитель – могут перекрываться в любых своих фазах, кроме операций advance (строки 23 и 42). Переменные inCnt и outCnt являются счетчиками событий – производства порции и потребления порции соответственно. Кроме того, каждый процесс хранит в собственной локальной переменной portNum номер порции, с которой ему предстоит работать (счет начинается с 1). Потребитель ждет, пока счетчик производств не достигнет номера очередной его порции, затем выбирает порцию из буфера и увеличивает счетчик потреблений. Производитель работает симметрично. Обратите внимание на второй параметр операции await в производителе (строка 37). Он задается таким, чтобы обеспечить отсутствие ожидания при наличии хотя бы одной свободной секции в буфере.
Другой механизм синхронизации носит название секвенсоров (sequencer). Буквальный перевод этого слова – "упорядочиватель"; так называются средства, которые выстраивают неупорядоченные события в определенном порядке. Как и счетчик событий, секвенсор представляется целым числом, над которым выполняется единственная операция: ticket. Операция ticket(S) возвращает текущее значение секвенсора и увеличивает его на 1. Операция является атомарной. Начальное значение секвенсора – 0.
Имея в своем распоряжении секвенсоры, мы можем так записать решение задачи производителей–потребителей для произвольного числа процессов:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
/* типы данных - счетчик событий и секвенсор */ typedef unsigned int eventcounter; typedef unsigned int sequencer; /* счетчики для чтения и записи */ static eventcounter inCnt = 0, outCnt = 0; /* секвенсоры для чтения и записи */ static sequencer inSeq = 0, outSeq = 0; /* буфер */ static portion buffer [BUFSIZE]; /* процесс-производитель */ void producer ( void ) { int portNum; /* номер порции */ /* рабочая область для порции */ portion work; /* цикл производства */ while (1) { < производство порции в work > /* получение "билета" на запись порции */ portNum = ticket (inSeq); /* ожидание номера порции */ await (inCnt, portNum); /* ожидание свободного места в буфере */ await (outCnt, portNum - BSIZE+1); /* запись в буфер */ memcpy (buffer + portNum % BSIZE, &work, sizeof(portion) ); /* продвижение счетчика чтения */ advance (inCnt); } } /* процесс-потребитель */ void consumer ( void ) { int portNum; /* номер порции */ /* рабочая область для порции */ portion work; /* цикл потребления */ while (1) { /* получение "билета" на выборку порции */ portNum = ticket (outSeq); /* ожидание номера порции */ await (outCnt, portNum); /* ожидание появления в буфере */ await (inCnt, portNum+1); /* выборка порции */ memcpy (&work, buffer + portNum % BSIZE, sizeof(portion) ); /* продвижение счетчика записи */ advance (outCnt); < обработка порции в work> } } |
Каждый производитель получает "билет" со своим номером в очереди на запись в буфер (строка 22). Затем он ожидает, когда до него дойдет очередь (строка 24), ожидает освобождения места в буфере (строка 27), записывает информацию (строки 29, 30) и наращивает счетчик производств (строка 32). Увеличение счетчика событий inCnt является сигналом к разблокированию как для потребителя, получившего "билет" на выборку этой порции и ожидающего в строке 46, так и для производителя, получившего "билет" на запись следующей порции и ожидающего в строке 27. Полученный процессом "билет" определяет и адрес в буфере той секции, с которой будет работать процесс. Хотя каждый процесс работает со своей секцией в буфере, одновременный доступ к буферу однотипных процессов исключается ожиданием в строке 24 или 46. Если разрешить одновременный доступ к буферу двух, например, производителей, то процесс, получивший "билет" на запись порции в n-ю секцию буфера может закончить запись раньше, чем процесс, пишущий порцию в n-1-ю секцию, даже если последний начал запись раньше. Процесс, закончивший запись, увеличит счетчик inCnt и выйдет из ожидания потребитель, имеющий билет на n-1-ю секцию, запись в которую еще не закончена.