Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учеб Пособ_Гончаровский.doc
Скачиваний:
1316
Добавлен:
29.03.2015
Размер:
3.65 Mб
Скачать

2.5.3. Процессы и передача сообщений

Процессы есть императивные программы с собственным адресным пространством. Эти программы не могут ссылаться на переменные друг друга и следовательно не имеют проблем потоков. Взаимодействия между программами происходят через механизмы ОС, микроядра или библиотеки.

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

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

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

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

Пример простой программы передачи сообщений приведен на рис. 82.

Эта программа использует шаблон producer/consumer, в которой один поток генерирует последовательность сообщений, а второй поток их потребляет. Такой шаблон может быть использован для реализации рассмотренного ранее шаблона observer без риска взаимной блокировки и без коварных ошибок предыдущих разделов. Функция update всегда выполняться в различных потоках наблюдателей и генерирует сообщения, потребляемые наблюдателями.

1 void* producer(void* arg) {

2 int i;

3 for (i = 0; i < 10; i++) {

4 send(i);

5 }

6 return NULL;

7 }

8 void* consumer(void* arg) {

9 while(1) {

10 printf("received %d\n", get());

11 }

12 return NULL;

13 }

14 int main(void) {

15 pthread_t threadID1, threadID2;

16 void* exitStatus;

17 pthread_create(&threadID1, NULL, producer, NULL);

18 pthread_create(&threadID2, NULL, consumer, NULL);

19 pthread_join(threadID1, &exitStatus);

20 pthread_join(threadID2, &exitStatus);

21 return 0;

22 }

Рис.82. Пример простой программы передачи сообщений

Функция producer (стартовая функция потока-отправителя) в строке 4 вызывает send (должна быть определена) для отправки сообщения в виде целого значения. Функция consumer обеспечивается тем что, get не возвратится пока она реально не получила сообщение. Заметим, что в этом случае consumer никогда не вернется. Эта программа не завершается собственными средствами.

Реализация send и get с использованием Pthreads приведена на рис. 83.

Реализация использует связанный список подобный рис. 78, но нагрузка является целой величиной. Связанный список реализован как неограниченная очередь с дисциплиной FIFO (first-in, first-out), когда новый элемент помещается в tail (хвост), а старые элементы удаляются из head (головная часть).

Рассмотрим первой реализацию send. Она использует мутекс для того чтобы send и get одновременно не модифицировали связанный список. В дополнении она использует переменную условия для взаимодействия с процессом consumer, который изменяет размер очереди. Переменная условия sent объявляется и инициализируется в строке 7. В строке 23 поток producer вызывает функцию pthread_cond_signal, которая «пробуждает» другой поток, блокированный переменной условия, если такой поток существует.

1 #include <pthread.h>

2 struct element {int payload; struct element* next;};

3 typedef struct element element_t;

4 element_t *head = 0, *tail = 0;

5 int size = 0;

6 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

7 pthread_cond_t sent = PTHREAD_COND_INITIALIZER;

8

9 void send(int message) {

10 pthread_mutex_lock(&mutex);

11 if (head == 0) {

12 head = malloc(sizeof(element_t));

13 head->payload = message;

14 head->next = 0;

15 tail = head;

16 } else {

17 tail->next = malloc(sizeof(element_t));

18 tail = tail->next;

19 tail->payload = message;

20 tail->next = 0;

21 }

22 size++;

23 pthread_cond_signal(&sent);

24 pthread_mutex_unlock(&mutex);

25 }

26 int get(void) {

27 element_t* element;

28 int result;

29 pthread_mutex_lock(&mutex);

30 while (size == 0) {

31 pthread_cond_wait(&sent, &mutex);

32 }

33 result = head->payload;

34 element = head;

35 head = head->next;

36 free(element);

37 size--;

38 pthread_mutex_unlock(&mutex);

39 return result;

40 }

Рис.83. Функции send и get для передачи сообщений

Чтобы увидеть что означает «пробуждает» другого потока, посмотрим на функцию get. В строке 31, если поток вызывающий get обнаружил, что размер очереди равен 0, тогда он вызывает pthread_cond_wait, который будет блокировать поток до тех пор, пока некоторый другой поток не вызовет pthread_cond_signal. (Существуют другие условия, которые заставят вернуться из pthread_cond_wait, так код должен периодически ждать пока не обнаружит отличный от нуля размер очереди).

Критично то, что функции pthread_cond_signal и pthread_cond_wait вызываются, пока владеют замком мутекса. Предположим, что строки 23 и 24 переставлены местами и pthread_cond_signal была вызвана после освобождения замка мутекса. Тогда в этом случае будет возможным вызов pthread_cond_signal пока поток consumer приостановлен (но еще не заблокирован) между строками 30 и 31. В этом случае, когда поток consumer разрешен для работы, будет исполнена строка 31 и наступит блокировка ожидания сигнала. Но сигнал уже был послан и он не может быть послан повторно, так что поток consumer будет постоянно блокироваться.

Заметим далее в строке 31, что pthread_cond_wait принимает в качестве аргумента &mutex. Пока поток блокируется на ожидании, он временно освобождает замок мутекса. Если не сделать этого, тогда поток producer не сможет попасть в критическую секцию и, следовательно, не сможет послать сообщение. Программа окажется в состоянии взаимоблокировки. Перед выходом из pthread_cond_wait, функция будет вновь получать замок мутекса. Программист должен быть очень аккуратным, когда вызывает pthread_cond_wait, т.к. замок мутекса временно освобожден во время вызова. Заметим что, значение некоторой разделяемой переменной после вызова pthread_cond_wait может быть отличным от значения до вызова.

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

В 60-е годы прошлого века Е. Дейкстра заимствовал эту идею, чтобы показать, как программы могут надежно разделять ресурсы. Вычислительный семафор (PV семафор Дейкстра) это неотрицательная целая (натуральная) переменная. Значение 0 рассматривается обособленно. Фактически переменная size из предыдущего примера является семафором. Она увеличивается при отправке сообщений, а величина 0 блокирует consumer, пока значение станет ненулевым. Переменная условия обобщает эту идею, поддерживая произвольные условия, а не только 0 или не 0, как критерий для блокировки. Более тога, по крайней мере, в Pthreads переменные условия координируются с мутексами для облегчения написания программ.

Использование передачи сообщений в приложениях может быть проще, чем использование потоков разделяемых переменных, но даже и в этом случае существуют опасности. Реализация шаблона producer/consumer фактически имеет четкий дефект. В частности не налагаются ограничения на размер очереди сообщений. В некоторый момент времени поток producer вызывает send, будет выделена памяти для запоминания сообщения, память не будет возвращена, пока сообщение не употребится. Если поток producer генерирует сообщения быстрее, чем consumer потребляет, то программа, в конце концов, исчерпает допустимую память. Это может быть зафиксировано ограничением размера буфера, но какой размер является приемлемым?

Выбор маленького буфера может вызвать взаимоблокировку, большого буфера – неэкономно. Эта проблема не имеет тривиального решения.

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

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