начинают зависеть от способности нашего ума к эволюции. Мы становимся частью программного, а не аппаратного обеспечения.»
При реализации параллелизма в программном обеспечении необходимо следовать одному важном)- правилу: параллелизм нужно обнаружить, а не внести извне. Иногда цель увеличения быстродействия программы не является достаточно оп-равданной для насаждения параллелизма в логику программы, которая по своей природе является последовательной. Параллелизм в проекте должен быть естественным следствием требований системы. Если параллельность определена в технических требованиях ксистеме, то следует с самого начала рассматривать варианты архитектуры и алгоритмы, которые поддерживают параллелизм. В противном случае необходимость паралле-лизма «всплывет» в уже существующей системе, которая изначально была нацелена лишь на выполнение последовательных действий. Такал участь часто постигает системы, которые первоначально разрабатывались как однопользовательские, а затем постепенно вырастали во многопользовательские, или системы, которые с функциональной точки зрения слишком далеко отошли от исходных спецификаций. В таких системах намерение внести в систему параллелизм можно сравнить с попыткой «махать руками после драки», и в этом случае для поддержки параллельности остается лишь делать архитектурные «пристройки». В этой книге мы описываем методы реализации естественного параллелизма. Другими словами, если мы знаем, что нам нужно обеспечить параллелизм, нас интересует, как это сделать, используя средства С++?
Мы представляем архитектурный подход к управлению параллелизмом в программе, используя преимущества С++-поддержки объектно-ориентированного программирования и универсальности. В частности, С++-средства поддержки наследования, полиморфизма и шаблонов успешно применяются для реализации архитектурных решений и программных компонентов, которые поддерживают параллельность. Методы объектно-ориентированного программирования обеспечивают поддержку десяти типов классов, перечисленных в табл. 11.1.
Таблица 11.1. Типы объектно-ориентированных классов
Безусловно, эти типы классов особенно полезны для проектов, в которых предполагается реализовать параллельность. Дело в том, что они позволяют внедрить принцип компоновки из стандартных блоков. Мы обычно начинаем с примитивных компонентов, используя их для построения классов синхронизации. Классы синхронизации позволят нам создавать контейнерные и каркасные классы, рассчитанные на безопасное внедрение параллелизма. Каркасные классы представляют собой строительные блоки, предназначенные для таких параллельных архитектур более высокого уровня, как мультиагентные системы и «доски объявлений». На каждом уровне сложность параллельного и распределенного программирования уменьшается благодаря использованию различных типов классов, перечисленных в табл. 11.1.
Итак, начнем с интерфейсного класса. Интерфейсный (или адаптерный) класс испоользуется для модификации или усовершенствования интерфейса другого класса или множества классов. Интерфейсный класс может также выступать в качестве оболочки, созданной вокруг одной или нескольких функций, которые не являются членами конкретного класса Такая роль интерфейсного класса позволяет обеспечить обьектно-ориентированный интерфейс с программным обеспечением, которое необязательно является объектно-ориентированным. Более того, интерфейсные классы позволяют упростить интерфейсы таких библиотек функций, как POSIX threads, PVM и MPI. Мы можем «обернуть» необъектно-ориентированную функцию в объектно-ориеитированный интерфейс; либо мы можем «обернуть» в интерфейсный класс некоторые данные, инкапсулировать их и предоставить им таким образом объектно-ориентированный интерфейс. Помимо упрощения сложности некоторых библиотек функций, интерфейсные классы используются для обеспечения разработчиков ПО согласующимся интерфейсом API (Application Programmer Interface). Например, С++-программисты, которые привыкли работать с iostream-классами, получат возможность выполнять операции ввода-вывода, оперируя категориями обьектно-ориентированпых потоков данных. Кривая обучения существенно минимизируется, если новые методы ввода-вывода описать в виде привычного iostream-представлеиия. Например, мы можем представить библиотеку средств передачи сообщений MPI как коллекцию потоков.
mpi_stream Stream1;
mpi_stream Stream2;
Streaml << Messagel << Message2 << Message3;
Stream2 >> Message4;
//. . .
Нри таком подходе программист может целиком сосредоточиться на логике программы и не ломать голову над соблюдением требований к синтаксису библиотеки MPI.
Как воспользоваться преимуществами интерфейсных классов
Зачастую полезно использовать инкапсуляцию, чтобы скрыть детали библиотек функций и обеспечить создание самодостаточных компонентов, которые годятся для многократного использования. Возьмем для примера мьютекс, который мы рассматривали в главе 7. Вспомним, что мьютекс— это переменная специального типа, ис-пользуемая для синхронизации. Мьютексы позволяют получать безопасный доступ к критическом) разделу данных или кода программы. Существует шесть основных функций, предназначенных для работы с переменной типа pthread_mutex_t (POSIX Threads Mutex).
Все эти функции принимают в качестве параметра указатель на переменную типа pthread_mutex_t. Для инкапсуляции доступа к переменной типа pthread_mutex_t и упрощения вызовов функций, которые обращаются к мьютексным переменным, можно использовать интерфейсный класс. Рассмотрим листинг 11.1, в котором объявляется класс mutex.
// Листинг 11.1. Объявление класса mutex
class mutex{ protected:
pthread_mutex_t *Mutex;
pthread_mutexattr_t *Attr; public: