{

myclass ob1,ob2;

ob1.set_a(10);

ob2.set_a(99);

cout << ob1.get_a() <<’’\n’’;

cout << ob2.get_a() <<’’\n’’;

return 0;

}

Как и следовало ожидать, программа выводит на экран величины 10 и 99. Переменная a в myclass является закрытой. Это означает, что она доступна только для функций членов myclass. Например, такое обращение вызовет ошибку

# include <iostream.h>

class myclass {

int a; // закрыто для myclass

public:

void set_a(int num);

int get_a();

};

main()

{

myclass ob1,ob2;

ob1.a=10; // ОШИБКА! К закрытому члену нет доступа

ob2.a=99; // ОШИБКА! К закрытому члену нет доступа

cout << ob1.get_a() <<’’\n’’;

cout << ob2.get_a() <<’’\n’’;

return 0;

}

Пример. Рассмотрим использование открытых переменных

# include < iostream. h>

class myclass {

public:

int a; // теперь а открыта и не нужны set_a() и get_a()

};

main()

{

myclass ob1,ob2;

ob1.a = 10;

ob2.a = 99;

cout << ob1.a << ‘’\n’’;

cout << ob2.a <<’’\n’’;

return 0;

}

Рассмотрим пример, в котором создается класс - stack, реализующий стек, использующийся для хранения символов:

# include <iostream. h>

# define SIZE 10

class stack {

char stck[SIZE]; // содержит стек

int tos; // индекс вершины стека

public:

void init(); // инициализация стека

void push(char ch); // помещает в стек символ

char pop(); // выталкивает из стека символ

};

void stack::init() // инициализация стека

{

tos=0;

}

void stack::push(char ch) // помещение символа в стек

{

if ( tos==SIZE) {

cout << ‘’Стек полон’’;

return;

}

stck[tos] = ch;

tos++;

}

char stack :: pop() // выталкивание символа из стека

{

if(tos==0) {

cout <<’’Стек пуст’’;

return 0; // Возврат нуля при пустом стеке

}

tos--;

return stck[tos];

}

main()

{

stack s1,s2; // создание двух стеков

int i;

s1.init(); // инициализация стека

s2.init();

s1.push(‘a’); // заполнение стека

s2.push(‘x’);

s1.push(‘b’);

s2.push(‘y’);

for(i=0;i<2;i++) cout <<’’символ из s1:’’<<s1.pop() <<’’\n’’; // выталкивание из стека

for(i=0;i<2;i++) cout <<’’символ из s2:’’<<s2.pop() <<’’\n’’;

return 0;

}

Эта программа выводит на экран следующее:

символ из s1: b

символ из s1: a

символ из s2: y

символ из s2: x.

Полиморфизм

Полиморфизм - это свойство, которое позволяет одно и то же имя использовать для решения двух или более задач, но технически разных. Целью полиморфизма, применительно к ООП, является использование одного имени для задания общих для класса действий. Выполнение каждого конкретного действия будет определяться типом и/или количеством данных. Например, в языке С++ можно использовать одно имя функции для множества различных действий. Это называется перегрузкой функции. В более общем смысле концепцией полиморфизма является идея «один интерфейс, множество методов». Это означает, что можно создать общий интерфейс для группы близких по смыслу действий. При этом выполнение конкретного действия зависит от данных. Преимуществом полиморфизма является то, что он помогает снижать сложность программ. Выбор же конкретного действия, в зависимости от ситуации, возлагается на компилятор. Полиморфизм может применяться и к функциям, и к операциям.

Остановимся на понятии перегрузка функций. Перегрузка функций не только обеспечивает тот механизм, посредством которого в С++ достигается один из типов полиморфизма, она также формирует то ядро, вокруг которого развивается среда программирования. Перегрузить функцию очень легко: просто объявите и задайте все требуемые варианты. Компилятор автоматически выберет правильный вариант вызова на основании числа или /и типа используемых в функции аргументов. То есть, имеем один интерфейс и множество методов.

Рассмотрим пример определения абсолютного значения числа, представляемого как целое, как длинное целое и как число с плавающей точкой на основе введения перегрузки функции.

# include < iostream.h >

// Перегрузка abs() тремя способами

int abs( int n);

long abs( long n);

double abs( double n);

main()

{

cout << ‘’ Абсолютная величина -10:’’ << abs(-10) << ‘’\n’’;

cout << ‘’ Абсолютная величина -10L:’’ << abs(-10L) << ‘’\n’’;

cout << ‘’ Абсолютная величина -10.01:’’ << abs(-10.01) << ‘’\n’’;

return 0;

}

int abs( int n)

{ return n < 0 ? - n : n; }

long abs(long n)

{ return n < 0 ? - n : n;}

double abs(double n)

{ return n < 0 ? -n : n;}

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

НЕ нашли? Не то? Что вы ищете?

# include < iostream.h>

void f1( int a);

void f1( int a, int b);

main()

{

f1(10);

f1(10,20);

return 0;

}

void f1( int a)

{

cout << ‘’В f1:’’<<a<<’’ \n’’;

}

void f1( int a, int b)

{

cout <<’’ В f1:’’<<a<<b’’ \n’’;

}

Конструкторы и деструкторы

Фактически для каждого создаваемого объекта на практике необходима инициализация. Для разрешения этой проблемы С++ предоставляет функцию - конструктор, включаемую в описание класса. Конструктор класса вызывается автоматически каждый раз при создании объекта этого класса.

Конструктор имеет то же имя, что и класс, частью которого он является, и не имеет возвращаемого значения. Рассмотрим сказанное на примере:

# include < iostream.h>

class myclass {

int a;

public:

myclass(); // конструктор

void show();

};

myclass:: myclass()

{

cout <<’’В конструкторе \n’’;

a=10;

}

void myclass::show()

{

cout << a;

}

main()

{

myclass ob;

ob. show();

return 0; }

В этом примере значение а инициализируется конструктором myclass(). Конструктор вызывается тогда, когда создается объект ob. Важно помнить, что в С++ оператор объявления является оператором действия.

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

# include <iostream. h>

class myclass {

int a;

public:

myclass(); // конструктор

~myclass(); // деструктор

void show();

};

myclass::myclass()

{

cout << ‘’Cодержимое конструктора \n’’;

a = 10;

}

myclass::~myclass()

{

cout <<’’Удаление...\n’’;

}

void myclass::show()

{

cout << a << ‘’\n’’;

}

main()

{

myclass ob;

ob. show();

return 0;

}

В данном примере деструктор класса вызывается при окончании программы main(). Локальные объекты удаляются тогда, когда они выходят из поля видимости. Глобальные объекты удаляются при завершении программы. Применение конструкторов и деструкторов для действий, прямо не связанных с инициализацией, является очень плохим стилем программирования и его следует избегать. Рассмотрим применение конструктора и деструктора при создании простого класса для строк, который содержит саму строку и ее длину. Когда создается объект strtype, для хранения строки выделяется память, и начальная длина строки устанавливается равной начальному значению. Когда объект strtype удаляется, эта память освобождается. Будем использовать конструктор с параметром. Рассмотрим пример:

# include <iostream. h>

# include <malloc. h>

# include <string. h>

# include <stdlib. h>

class strtype

{

char *p; // p - указатель на переменную типа char

int len;

public:

strtype(char *ptr);

~strtype();

void show();

};

strtype :: strtype(char *ptr)

{

len = strlen(ptr);

p = (char *) malloc(len + 1);

if (!p) {

cout << ‘’ Ошибка выделения памяти \n’’;

exit(1);

}

strcpy(p, ptr);

}

strtype :: ~strtype()

{

cout << ‘’ Освобождение p\n’’;

free(p);

}

void strtype :: show()

{

cout << p<< ‘’-длина: ‘’ << len;

cout << ‘’\n’’;

}

main()

{

strtype s1(‘’Это проверка’’), s2(‘’ Мне нравится С++’’);

s1.show();

s2.show();

return 0;

}

Наследование

Наследование - это процесс, посредством которого один объект может приобретать свойства другого. Точнее, объект может наследовать основные свойства другого объекта и добавлять к ним черты, характерные только для него. Наследование является важным, поскольку оно позволяет поддерживать концепцию иерархии классов. Применение иерархии классов делает управляемыми большие потоки информации. Когда один класс наследуется другим, класс который наследуется, называется базовым классом. Наследующий класс называется производным классом. Обычно процесс наследования начинается с задания базового класса. Базовый класс определяет все те качества, которые будут общими для всех производных классов. Рассмотрим пример:

class base { // Задание базового класса

int i;

public:

void set_i(int n);

int get_i();

};

class derived: public base { // Задание производного класса

int j;

public:

void set_j(int n);

int mul();

};

void base ::set_i(int n)

{ i = n; } // Установка значения i в базовом классе

int base::get_i()

{ return i; } // Возврат значения i в базовом классе

void derived::set_j(int n)

{ j = n; } // Установка значения j в производном классе

int derived::mul()

{ return j * get_i(); } // Вызов значения i из base и, одновременно, j из derived

main()

{

derived ob;

ob. set_i(10); // загрузка i в base

ob. set_j(4); // загрузка j в derived

cout << ob.mul(); // вывод числа 40

return 0;

}

Отметим, что функция get_i(), которая является членом базового класса, а не производного, вызывается внутри производного класса без какой бы то ни было связи с каким-либо объектом. Это возможно потому, что открытые члены класса base становятся открытыми членами derived.

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

Объединения, встраиваемые функции

Остановимся на важном понятии как объединение, которое наряду с классами и структурами часто используется при программировании на С++, кроме того оно очень наглядно демонстрирует мощь объектно-ориентированного программирования. В С++ объединение также определяет тип класса, в котором функции и данные могут содержаться в виде членов. Объединение похоже на структуру тем, что в нем, по умолчанию, все члены открыты до тех пор, пока не используется спецификатор private.

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

для любого другого типа. Объединения не должны содержать объектов с конструктором и деструктором. Рассмотрим пример декларирования объединения в С++:

# include <iostream.h>

union bits

{

bits(double n);

void show_bits();

double d;

}

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

Рассмотрим применение и назначение встраиваемых функций. В С++ можно задать функцию, которая, фактически, не вызывается, а ее тело встраивается в программу в месте ее вызова. Преимуществом встраиваемых функций является то, что они не связаны с вызовом функций и механизмом возврата. Это значит, что встраиваемые функции могут выполняться гораздо быстрее обычных. Недостатком встраиваемых функций является то, что если они слишком большие и вызываются слишком часто, объем использующих их программ сильно возрастает. Из-за этого применение встраиваемых функций обычно ограничивается короткими функциями. Для определения встраиваемой функции вписывается спецификатор inline перед определением функции. Рассмотрим сказанное на примере:

# include <iostream. h>

inline int even(int x)

{

return! (x%2);

}

main()

{

int n;

cin >>n;

if (even(n)) cout << ‘’ число является четным \n’’;

else cout << ‘’ число является нечетным \n’’;

return 0;

}

В этом примере функция even(), которая возвращает истину при четном аргументе, объявлена как встраиваемая функция. Это означает, что строка

if (even(n)) cout << ‘’ число является четным \n’’:

функционально идентична строке:

if (! (10%2)) cout << ‘’ число является четным \n’’;

Этот пример указывает также на другую важную особенность использования встраиваемой функции: она должна быть задана до ее первого вызова. Отметим, что спецификатор inline является запросом, а не командой для компилятора. Очень важно подчеркнуть и то, что некоторые компиляторы не воспринимают функцию как встраиваемую, если она содержит статическую переменную (static), оператор цикла, оператор switch или go to или, если функция является рекурсивной. Если определение функции - члена достаточно короткое, это определение можно включить в объявление класса. Поступив таким образом, мы заставляем, если это возможно, функцию стать встраиваемой. При этом ключевое слово inline не используется. Рассмотрим сказанное на примере:

# include <iostream. h>

class samp

{

int i, j;

public:

samp(int a, int b);

int divisible () { return ! ( i % j );} // определение встраиваемой функции

}

samp :: samp ( int a, int b)

{

i = a;

j = b;

}

main

{

samp ob1 ( 10,2 ), ob2 ( 10,3 );

if ( ob1.devisible()) cout << ‘’ Число делится нацело \n’’;

else cout << ‘’ Число не делится нацело \n’’;

if ( ob2.devisible()) cout << ‘’ Число делится нацело \n’’;

else cout << ‘’ Число не делится нацело \n’’;

return 0;

}

Указатели и адреса

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

Унарный оператор & выдает адрес объекта, так что инструкция

p=&c;

присваивает адрес ячейки с переменной p . Говорят, что р указывает на с.

Унарный оператор * есть оператор раскрытия ссылки. Примененный к указателю он выдает объект, на который данный указатель ссылается. Предположим, что х и у - целые, а ip – указатель на int. Рассмотрим пример.

int x=1,y=2,z[10];

int *ip; // ip – указатель на int

ip=&x; // теперь ip указывает на х

y=*ip; // y теперь равен 1

*ip=0; // x теперь равен 0

ip=&z[0]; // ip теперь указывает на z[0]

Функции в языке Си в качестве своих аргументов получают значения параметров. Поэтому прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции нет. Например, в программе сортировки может понадобится функция swap, переставляющая местами два неупорядоченных элемента. Для их перестановки недостаточно написать swap(a,b);где функция swap определена следующим образом:

void swap(int x, int y) // НЕВЕРНО

{

int temp; temp=x;

x=y; y=temp;

}

Поскольку swap получает лишь копии значений переменных a и b, она не может повлиять на переменные a,b той программы, которая к ней обратилась. Чтобы получить желаемый эффект, надо вызывающей программе передать указатели на те значения, которые должны быть изменены: swap(&a,&b);Так как оператор & получает адрес переменной, &a – есть указатель на а. В самой функции swap параметры должны быть описаны как указатели, при этом доступ к значениям параметров будет осуществляться через них косвенно.

void swap( int *px, int *py)

{

int temp;

temp=*px;

*px=*py;

*py=temp;

}

Рассмотрим синтаксис декларирования переменных типа указатель, являющихся передаваемыми параметрами функции. Отметим, что в подобных случаях синтаксис подстраивается под синтаксис выражений, в которых такая переменная может встретиться. Например, запись:

double *dp, atof(char *);

означает, что выражение *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char. Полезно знать, что указателю разрешено ссылаться только на объекты заданного типа. Существует одно исключение, указатель на void может ссылаться на объекты любого типа, но к такому указателю нельзя применять оператор раскрытия ссылки. Если ip ссылается на х целого типа, то *ip можно использовать в любом месте, где допустимо применение х. Например, оператор:

*ip = *ip + 10;

увеличивает *ip на 10. Унарные операторы * и & имеют более высокий приоритет, чем арифметические операторы. Указатели сами являются переменными. В тексте программы они могут встречаться и без оператора раскрытия ссылки. Например, если iq есть указатель на int, то можно использовать оператор:

ip = iq;

который копирует содержимое iq в ip, чтобы iq и ip ссылались на один и тот же объект.

Рассмотрим связь указателей и массивов. Декларация:

int a[10];

определяет массив а размера 10, то есть блок из 10 последовательных объектов с именами а[0],a[1],…a[9]. Запись a[i] отсылает к i –му элементу массива. Если pa есть указатель на int, то есть определен как:

int *pa;

то в результате присваивания

pa = &a[0];

pa будет указывать на нулевой элемент а, иначе говоря, будет содержать адрес элемента а[0]. Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания:

ра = &a[0];

рa и a имеют одно и то же значение. Поскольку имя массива есть не что иное, как адрес его начального элемента, присваивание pa=&a[0]; можно также записать в следующем виде: pa=a; Интересно знать, что a[i] можно записать как *(a+i).

2.6. Программирование параллельных вычислений

Введение

Язык программирования mpC - это расширение языка Си, разработанное специально для программирования параллельных вычислений на обычных сетях разнородных компьютеров [5]. Основной целью параллельных вычислений является ускорение решения задачи. Именно это отличает параллельные вычисления от распределённых, для которых основной целью является обеспечить совместную работу программных компонент, изначально размещённых на различных компьютерах. В случае параллельных вычислений разбиение программы на компоненты, размещаемые на разных компьютерах, является лишь средством для ускорения работы программы, а не врождённым свойством этой программы. Поэтому, основное внимание в языке mpC уделяется средствам, позволяющим максимально облегчить разработку как можно более эффективных программ для решения задач на обыкновенных сетях компьютеров.

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

Для начала рассмотрим простейшую программу, которая выводит на терминал пользователя текст "Hello, world!". Код этой программы мало отличается от кода программы на Си. Первое отличие - спецификатор [*] перед именем main в определении главной функции. Он специфицирует вид функции, говоря о том, что код этой функции выполняется всеми процессами параллельной программы. Функции, подобные main, называются в mpC базовыми. Корректная работа таких функций возможна только при условии их вызова всеми процессами параллельной программы. Контроль за корректностью вызовов базовых функций осуществляется компилятором.

#include <stdio. h>

int [*]main() {

[host] printf ("Hello, world.\n");

}

Второе отличие - это конструкция [host] перед именем функции printf в выражении, где эта стандартная библиотечная функция языка Си вызывается. В отличие от функции main, для корректной работы функции printf не требуется ее параллельного вызова всеми процессами параллельной программы. Такие функции называются в mpC узловыми. Язык предоставляет возможность вызова узловой функции, как отдельным процессом параллельной программы, так и её параллельного вызова группой процессов. В нашем случае, функция printf выполняется только одним процессом параллельной программы, а именно, процессом, связанным с терминалом пользователя, из которого он запускал на выполнение эту параллельную программу. Ключевое имя host жёстко связано в языке mpC именно с этим процессом. Изменим программу таким образом, чтобы все процессы параллельной программы выполняли вызов функции printf. Библиотечная узловая функция MPC_Printf языка mpC гарантирует вывод приветствия на терминал пользователя от каждого компьютера, участвующего в процессе параллельной программы.

#include <mpc. h>

int [*]main()

{

MPC_Printf(" Hello, world! \n");

}

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

#include <mpc. h>

#include <sys/utsname. h>

int [*]main()

{

struct utsname un;

uname(&un);

MPC_Printf("Hello world! I'm on \"%s\".\n",

un. nodename);

}

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

После выполнения вызова узловой (библиотечной) функции uname, поле nodename структуры un будет содержать ссылку на имя компьютера, на котором этот процесс выполняется.

Каждый из процессов параллельной программы выполняет вызов функции uname, после чего поле nodename соответствующей проекции распределённой структуры un содержит ссылку на имя того компьютера, на котором этот процесс выполняется.

Значения распределённых переменных и распределённых выражений, таких как un. nodename или &un, естественным образом распределены по процессам параллельной программы и называются распределённми значениями. Следующая программа расширяет вывод программы информацией об общем числе процессов параллельной программы.

#include <mpc. h>

#include <sys/utsname. h>

int [*]main()

{

struct utsname un;

repl int one;

repl int number_of_processes;

uname(&un);

one = 1;

number_of_processes = one[+];

MPC_Printf("Hello world! I'm one of %d processes"

"and run on \"%s\".\n",

number_of_processes, un. nodename);}

Для этого в программе определены две целые распределённые переменные one и number_of_processes. Сначала в результате выполнения присваивания one = 1 всем проекциям переменной one присваивается значение 1. Результатом применения постфиксной операции [+] к переменной one будет распределённое значение, проекция которого на любой из процессов равна сумме значений проекций переменной one. Другими словами, проекция значения выражения one [+] на любой из процессов параллельной программы будет равна их общему количеству. В результате присваивания этого распределённого значения распределенной переменной number_of_processes все проекции последней будут содержать одно и то же значение, а именно, общее число процессов параллельной программы.

Определение распределённой переменной one содержит ключевое слово repl (сокращение от replicated), которое информирует компилятор о том, что в любом выражении проекции значения этой переменной на разные процессы параллельной программы равны между собой. Такие распределённые переменные называются в mpC размазанными (соответственно, значение размазанной переменной называется размазанным значением). Размазанные переменные и выражения играют большую роль в mpC. Компилятор контролирует объявленное программистом свойство размазанности и предупреждает обо всех случаях, когда оно может быть нарушено.

Заметим, что более простая следующая программа обеспечивает такой же результат, что и предыдущая программа, используя библиотечную узловую функцию MPC_Total_nodes языка mpC.

#include <mpc. h>

#include <sys/utsname. h>

int [*]main()

{

struct utsname un;

uname(&un);

MPC_Printf("Hello world! I'm one of %d processes" "and run on \"%s\".\n",

MPC_Total_nodes(), un. nodename);

}

Эта программа эффективней предыдущей программы еще и тем, что параллельный вызов функции MPC_Total_nodes, в отличие от вычисления выражения one [+], не требует обмена данными между процессами программы.

Сети. Родитель сети

Программы, которые мы до сих пор рассматривали, описывали вычисления, в которых участвовали либо все процессы параллельной программы, либо только хост - процесс. Очень часто число процессов, вовлечённых в параллельное решение задачи, зависит от самой задачи и/или параллельного алгоритма её решения и определяется входными данными. Например, если для параллельного моделирования движения N групп тел под влиянием взаимного притяжения используется отдельный процесс для каждой группы тел, то в соответствующие параллельные вычисления должны быть вовлечены в точности N процессов. Запуск параллельной программы осуществляется средствами внешними по отношению к языку mpC. Общее число составляющих её процессов не определяется разработчиком программы на mpC. У разработчика есть лишь языковые средства для определения этого числа.

Рассмотрим программу, которая даёт первое знакомство с языковыми средствами, позволяющими программисту описывать параллельные вычисления на нужном числе процессов. Сами вычисления, по-прежнему, предельно просты - каждый из участвующих в них процессов просто выводит на терминал пользователя приветствие "Hello, world!". Число же участвующих процессов (N =3 ) будет задаваться программистом.

Группе процессов, совместно выполняющих некоторые параллельные вычисления, в языке mpC соответствует понятие сети. Сеть в mpC - это абстрактный механизм, облегчающий программисту работу с реальными физическими процессами параллельной программы.

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

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

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8