Параллельное и распределенное программирование на С++
Шрифт:
Чтобы понять, как подойти к созданию «полуширокого» интерфейса, построим интерфейсный класс для POSIX-семафора. И хотя семафор не является частью библиотеки Pthread, он находит аналогичные применения в многопоточной среде. Его можно использовать в среде, которая включает параллельно выполняемые процессы и потоки. Поэтому в некоторых случалх требуется объект синхронизации более общего характера, чем наш класс mutex.
Определение класса semaphore показано в листинге 11.10.
// Листинг 11.10. Объявление класса semaphore
class semaphore : public synchronization_variable( protected:
sem_t * Semaphore; public://.. .
int lock(void);
int unlock(void);
int trylock(void);
//. . .
};
Синопсис
<semaphore.h>
int sem_init(sem_t *, int, unsigned int) ;
int sem_destroy(sem_t *);
sem_t *sem_open(const char *, int, ...);
int sem_close(sem_t *);
int sem_unlink(const char *);
int sem_wait(sem_t *);
int sem_trywait(sem_t *);
int sem_post(sem_t *);
int sem_getvalue(sem__t *, int *);
Обратите
// Листинг 11.11. Определение методов lock, unlock и
// trylock для класса semaphore
int semaphore::lock(void) (
//.. .
return(sem_wait(Semaphore));
}
int semaphore::unlock(void) {
//. . .
return(sem_post(Semaphore));
}
Итак, теперь функции lock , unlock , trylock и тому подобные заключают в оболочку семафорные функции библиотеки POSIX, а не функции библиотеки Pthread. Важно отметить, что семафор и мьютекс — не одно и то же. Но их можно использовать в аналогичных ситуациях. Зачастую с точки зрения инструкций, которые реализуют параллелизм, механизмы функций lock и unlock имеют одно и то же назначение. Некоторые основные различия между мьютексом и семафором указаны в табл. 11.2.
Таблица 11.2. Ос н овные различия между мью т ексами и семафорами
• Характеристики мьютексов
• Мьютексы и переменные условий разделяются между потоками
• Мьютекс деблокируется теми же потоками, которые его заблокировали
• Мьютекс либо блокируется, либо деблокируется
• Характеристики семафоров
• Семафоры обычно разделяются между процессами, но их разделение возможно и между потоками
• - Освобождать семафор должен необязательно тот процесс или поток, который его удерживал
• Семафоры управляются количеством ссылок. Стандарт POSIX включает именованные семафоры
Несмотря на важность различий в семантике (см. табл. 11.2), часто их оказывается недостаточно для оправдания применения к семафорам и мьютексам совершенно различных интерфейсов. Поэтому мы оставляем «полуширокий» интерфейс для функций lock, unlock и trylock с одним предостережением: программист должен знать различия между мьютексом и семафором. Это можно сравнить с ситуацией, которая возникает с такими «широкими» интерфейса м и таких контейнерных классов, как deque, queue, set, multiset и пр. Эти контейнерные классы связаны общим интерфейсом, но их семантика в определенных областях различна. Используя понятие интерфейсного класса, можно разработать соответствующие компоненты синхронизации для мьютексов, переменных условий, мьютексов чтения-записи и семафоров. Имея такие компоненты, мы можем спроектировать безопасные (с точки зрения параллелизма) контейнерные, доменные и каркасные классы. Мы можем также применять интерфейсные классы для обеспечения единого интерфейса с различными версиями одной и той же библиотеки функций при необходимости использования обеих версий (по разным причинам) в одном и том же приложении. Иногда интерфейсный класс может быть успешно применен для безболезненного перехода от устарелых функций к новым. Если мы хотим оградить программиста от различий, существующих между операционными системами, то наша цель — обеспечить его соответствующим АРI-интерфейсом [18], независимо от того, какая библиотека семафорных функций используется в среде: System V или POSIX.
Поддержка потокового представления
Помимо использования интерфейсных классов для упрощения программирования и создания новых «широких» интерфейсов библиотек средств параллелизма и передачи сообщений, имеет смысл также расширить существующие интерфейсы. Например, объектно-ориентированное представление потоков данных можно расширить за счет использования каналов, FIFO-очередей и таких библиотек передачи сообщений, как PVM и MPI. Эти компоненты используются ради достижения межпроцессного взаимодействия (Inter-Process Communication — IPC), межпотокового взаимодействия (Inter-Thread Communication — ITC), а в некоторых случалх и взаимодействия между объектами (Object-to-Object Communicaton — OTOC). Если взаимодействие имеет место между параллельно выполняемыми потоками или процессами, то канал связи может представлять собой критический раздел. Другими словами, если несколько процессов (потоков) попытаются одновременно
// Листинг 11.12. Объявление класса pvm_stream, который
// наследует класс mios
class pvm_stream : public mios{
protected:
int TaskId;
int MessageId;
mutex Mutex;
//...
public:
void taskId(int Tid);
void messageId(int Mid);
pvm_stream(int Coding=PvmDataDefault);
void reset(int Coding = PvmDataDefault);
pvm_stream &operator<<(string &Data);
pvm_stream &operator>>(string &Data);
pvm_stream &operator>>(int &Data);
pvm_stream &operator<<(int &Data);
//. . .
};
Этот класс обработки потоков данных предназначен для инкапсуляции состояния активного буфера в PVM-задаче. Операторы вставки "<<" и извлечения ">>" можно использовать для отправки и приема сообщений между PVM-процессами. Здесь мы рассмотрим использование этих операторов только для обработки строк и значений типа int. Интерфейс этого класса далек от совершенства. Поскольку этот класс предназначен для обработки данных любого типа, мы должны расширить определения операторов "<<" и ">>". А так как мы планируем использовать класс pvm_stream в многопоточной программе, мы должны быть уверены в том, что объект класса pvm_stream безопасен для потоков. Поэтому мы включаем в качестве члена нашего класса pvm_stream класс mutex. Поскольку сообщение может быть направлено для конкретной PVM-задачи, класс pvm_stream инкапсулирует для нее активный буфер. Наша цель — использовать классы ostream и istream в качестве «путеводителя» по функциям, которые должен иметь класс pvm_stream. Вспомним, что классы ostream и istream являются классами трансляции. Они переводят типы данных в обобщенные потоки байтов при выводе и обобщенные потоки байтов в конкретные типы данных при вводе. Используя классы istream и ostream, программисту не нужно погружаться в детали вставки в поток или выделения из потока данных того или иного типа. Мы хотим, чтобы и поведение класса pvm_stream было аналогичным. Библиотека PVM располагает различными функциями для каждого типа данных, которые необходимо упаковать в буфер отправки или распаковать из буфера приема. Например, функции pvm_pkdouble pvm_pkint pvm_pkfloat используются для упаковки double-, int- и float-значений соответственно. Аналогичные функции существуют и для других типов данных, определенных в С++. Мы бы хотели поддерживать наше потоковое представление, т.е. чтобы ввод и вывод данных можно было представить как обобщенный поток байтов, который перемещается в программу или из нее. Следовательно, мы должны определить операторы вставки (<<) и извлечения (>>) для каждого типа данных, который мы собираемся использовать при обмене сообщениями между PVM-задачами. Мы также моделируем состояние потока данных в соответствии с классами istream и ostream, которые содержат компонент ios, предназначенный для хранения состояния этого потока. Поток данных может находиться в состоянии ошибки либо в одном из различных состояний, которые выражаются восьмеричным, десятичным или шестнадцатеричным числом. Поток также может пребывать в нормальном, заблокированном или состоянии конца файла. Класс pvm_stream должен не только содержать компонент, который поддерживает состояние потока данных, но и методы, которые устанавливают заданное или исходное состояние PVM-задачи, а также считывают его. Наш класс pvm_stream для этих целей содержит компонент mios. Этот компонент поддерживает состояние потока данных и активного буфера отправки и приема информации. На рис. 11.4 представлены две диаграммы классов: одна отображает отношения между основными классами библиотеки iostream, а вторая — отношения между классом pvm_stream и ero компонентами.
Обратите внимание на то, что классы istream и ostream наследуют класс ios . Класс ios поддерживает состояние потока данных и состояние буфера, используемого классами istream и ostream. Наш класс mios исполняет ту же роль в отношении класса pvm_stream. Классы istream и ostream содержат определения операторов "<<" и ">>". Эти же операторы определены и в нашем классе pvm_stream. Поэтому, хотя наш класс pvm_stream не связан с iostream-классами наследованием, между ними существует интерфейсная связь. Мы используем интерфейс iostream-классов в качестве «полуширокого» интерфейса для классов pvm_stream и mios. Обратите внимание на то, что класс mios (см. рис. 11.4) наслелуется классом pvm_stream. Если мы хотим поддерживать потоковое представление с помощью класса pvm_stream, то для этого как раз подходит понятие интерфейсного класса.