Скачиваний:
71
Добавлен:
02.05.2014
Размер:
434.18 Кб
Скачать

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-ю секцию, запись в которую еще не закончена.

Соседние файлы в папке Системное программирование и операционные системы