Первым примером демонстрируется, как порождать задачи PVM и синхронизировать их. Программа порождает несколько задач - по умолчанию три. После этого потомок синхронизируется путем передачи сообщения своей задаче-предку. Предок принимает сообщения от каждой порожденной им задачи и выводит на экран информацию, заключенную в этих сообщениях от задач-потомков.
Программа ``раздваивание - присоединение'' (рис. ) содержит код как задачи-предка, так и задачи-потомка. Самое первое действие, которое делает программа - вызов функции pvm_mytid(). Функция должна вызываться для того, чтобы впоследствии можно было выполнять остальные вызовы PVM. В результате pvm_mytid() всегда должно возвращаться положительное целое число. Если это не так, то что-то осуществлено неправильно. В примере ``раздваивание - присоединение'' проверяется значение mytid; если оно отражает ошибку, то вызывается pvm_perror() и осуществляется выход из программы. Вызов pvm_perror() выведет на экран сообщение, показывающее, что при последнем вызове PVM имела место ошибка. В данном случае, последним вызовом был pvm_mytid(), так что pvm_perror может вывести на экран сообщение о том, что PVM не смогла запуститься на текущей машине. Аргумент pvm_perror() - это строка, которой при выводе на экран непосредственно будет предшествовать само сообщение об ошибке. В данном случае передается argv[0], который содержит имя программы в том виде, в котором оно было введено через командную строку. Функция pvm_perror() вызывается после функции UNIX perror().
Допустим, что в результате получается достоверное значение mytid. Теперь вызывается pvm_parent(). Функция pvm_parent() вернет TID задачи, которая породила задачу, сделавшую вызов. Начиная с того момента, как была запущена на выполнение инициирующая программа ``раздваивание-присоединение'' из оболочки UNIX, она не имеет предка; эта инициирующая задача не будет порождаться другими задачами, но может снова запускаться пользователями вручную. Для инициирующей задачи ``раздваивание - присоединение'' результатом pvm_parent будет не определенный идентификатор задачи, а код ошибки PvmNoParent. Таким образом - проверкой эквивалентности результата вызова pvm_parent() значению PvmNoParent - можно отличить задачу-предка ``раздваивание - присоединение'' от потомка. Естественно, если задача является предком, то она должна породить потомка. Если же она предком не является, то должна послать предку сообщение.
Количество задач определяется посредством командной строки - аргументом argv[1]. Если указанное количество задач недопустимо, то программа завершается, вызывая pvm_exit(). Вызов pvm_exit() очень важен, потому что он сообщает PVM, что программа не будет больше пользоваться ее услугами. (В подобном случае, задача выходит из PVM, а PVM сделает вывод о том, что задача ``умерла'' и больше не требует обслуживания. Так или иначе - это правильный стиль ``чистого'' завершения.) Приняв, что количество задач допустимо, после этого программа ``раздваивание - присоединение'' будет пытаться породить потомка.
Вызов pvm_spawn() приказывает PVM запустить ntask
задач с именами argv[0]. Второй параметр - список аргументов
для передачи порождаемым задачам. В данном случае не нужно заботиться
о передаче потомкам частных аргументов через командную строку, поэтому
он равен NULL. Третий параметр при порождении -
PvmTaskDefault
- флаг, приказывающий PVM породить задачи в ``местах'' по умолчанию.
Если бы было необходимо размещение потомков на специфических машинах
или на машинах с особенной архитектурой, то можно было бы воспользоваться
значением флага PvmTaskHost или PvmTaskArch, а четвертым
параметром указать хост или архитектуру. Поскольку не важно, где задачи
будут исполняться, используется значение PvmTaskDefault для
флага и значение NULL для четвертого параметра. Наконец,
через ntask сообщается число задач для запуска; целочисленный
массив для потомков будет содержать идентификаторы задач вновь порожденных
потомков. Возвращаемое pvm_spawn() значение отражает, сколько
задач было порождено успешно. Если info не соответствует
ntask, то в процессе порождения произошел ряд ошибок. В случае
с ошибкой ее код помещается в массив идентификаторов задач вместо
действительного идентификатора задачи соответствующего потомка. В
программе ``раздваивание - присоединение'' этот массив проверяется
с помощью цикла, а идентификаторы задач и возможные коды ошибок выводятся
на экран. Если ни одна задача не порождена успешно, то осуществляется
выход из программы.
От каждой задачи-потомка предок принимает сообщение и выводит его
содержимое на экран. Вызовом pvm_recv() принимается сообщение
(с JOINTAG) от любой задачи. Возвращаемое pvm_recv()
значение - это целое число, определяющее буфер сообщения. Это целое
число может быть использовано как информация для поиска буферов сообщений.
Последующий вызов pvm_bufinfo() определяет
длину, тег и идентификатор задачи передающего процесса для сообщения,
заданного с помощью buf. В программе ``раздваивание-присоединение''
сообщения, посланные потомками, содержат только по одному целочисленному
значению - идентификатору задачи соответствующей задачи-потомка. Вызовом
pvm_upkint() целое число распаковывается из сообщения в
переменную mydata. В программе
``раздваивание - присоединение'' тестируются значения
mydata
и идентификатора задачи, возвращенного pvm_bufinfo(). Если
эти значения различны, то программа содержит ошибку и сообщение об ошибке
выводится на экран. Наконец на экран выводится информация из сообщения,
и на этом работа программы-предка завершается.
Последний сегмент кода программы ``раздваивание - присоединение'' будет исполняться как задача-потомок. Чтобы можно было поместить данные в буфер сообщения, он должен быть инициализирован вызовом pvm_initsend(). Параметр PvmDataDefault говорит о том, что PVM должна преобразовать данные обычным способом, чтобы удостовериться, что они прибыли в формате, корректном для целевого процессора. В некоторых случаях нет необходимости в преобразовании данных. Если пользователь уверен, что целевая машина использует тот же формат данных и, действительно, преобразовывать данные нет нужды, то он может применить PvmDataRaw в качестве параметра pvm_initsend(). Вызов pvm_pkint() размещает единственное целое число - metid - в буфере сообщения. Важно быть уверенными, что соответствующий распаковочный вызов точно соответствует данному упаковочному. Если число упаковывается как целое, а распаковывается как число с плавающей запятой, то такой подход не корректен. Аналогично, если пользователь упаковывает два целых числа одним вызовом, то он не сможет их впоследствии распаковать двойным вызовом pvm_upkint() - по одному вызову на каждое целое число. Таким образом, между упаковочным и распаковочным вызовами должно существовать соответствие ``один к одному''. В завершение сообщение с тегом JOINTAG передается задаче-предку.
Пример "раздваивание - присоединение":
Пример ``раздваивание-присоединение''
Демонстрируется, как порождать процессы и
обмениваться сообщениями
*/
/* определения и прототипы библиотеки PVM */
#include <pvm3.h>
/* максимальное число потомков, которые будут
порождаться этой программой */
#define MAXNCHILD 20
/* тег для использования в сообщениях, связанных
с присоединением */
#define JOINTAG 11
int main(int argc, char* argv[]) {
/* количество задач для порождения, 3 используются
по умолчанию */
int ntask = 3;
/* код возврата для вызовов PVM */
int info;
/* свой идентификатор задачи */
int mytid;
/* свой идентификатор задачи-предка*/
int myparent;
/* массив идентификаторов задач-потомков*/
int child[MAXNCHILD];
int i, mydata, buf, len, tag, tid;
/* поиск своего идентификатора задачи */
mytid = pvm_mytid();
/* проверка на ошибки*/
if (mytid < 0) {
/* вывод на экран сообщения об ошибке*/
pvm_perror(argv[0]);
/* выход из программы */
return -1;
}
/* нахождение числа-идентификатора задачи-предка */
myparent = pvm_parent();
/* выход, если есть ошибки, но не PvmNoParent */
if ((myparent < 0 ) && (myparent != PvmNoParent() {
pvm_perror(argv[0]);
pvm_exit();
return -1;
}
/* если предка не найдено, то это и есть предок */
if (myparent == PvmNoParent) {
/* определение числа задач для порождения */
if (argc == 2) ntask = atoi(argv[1]);
/* удостоверение, что ntask - допустимо */
if ((ntask < 1) || (ntask > MAXNCHILD))
{pvm_exit();
return 0;}
/* порождение задач-потомков*/
info = pvm_spawn(argv[0], (char**)0, PvmTaskDefault,
(char*)0, ntask, child);
/* вывод на экран идентификаторов задач */
for (i = 0; i < ntask; i++)
if (child[i] < 0) /* вывод на экран десятичного
кода ошибки*/
printf(" %d", child[i]);
else /* вывод на экран шестнадцатеричного
идентификатора задачи */
printf("t%x\t", child[i]);
putchar('\n');
/* удостоверение, что порождение произошло успешно */
if (info == 0) {pvm_exit(); return -1;}
/* ожидание ответов только от тех потомков, которые
порождены успешно */
ntask = info;
for (i = 0; i < ntask; i++) {
/* прием сообщения от любого процесса-потомка */
buf = pvm_recv(-1, JOINTAG);
if (buf < 0) pvm_perror("calling recv");
info = pvm_bufinfo(buf, &len, &tag, &tid);
if (info < 0) pvm_perror("calling pvm_bufinfo");
info = pvm_upkint(&mydata, 1, 1);
if (info < 0) pvm_perror("calling pvm_upkint");
if (mydata != tid)
printf("Этого не может быть!\n");
printf("Длина %d, Tag %d, Tid t%x\n",
len, tag, tid);
}
pvm_exit();
return 0;
}
/* это потомок */
info = pvm_initsend(PvmDataDefault);
if (info < 0) {
pvm_perror("calling pvm_initsend"); pvm_exit();
return -1;
}
info = pvm_pkint(&mytid, 1, 1);
if (info < 0) {
pvm_perror("calling pvm_pkint"); pvm_exit();
return -1;
}
info = pvm_send(myparent, JOINTAG);
if (info < 0) {
pvm_perror("calling pvm_initsend"); pvm_exit();
return -1;
}
pvm_exit(); return 0;
}
Ниже показано, что выводит на экран выполняющаяся программа ``раздваивание - присоединание''.
% forkjoin
t10001c t40149 tc0037
Длина 4, Tag 11, Tid t40149
Длина 4, Tag 11, Tid tc0037
Длина 4, Tag 11, Tid t10001c
% forkjoin 4
t10001e t10001d t4014b tc0038
Длина 4, Tag 11, Tid t4014b
Длина 4, Tag 11, Tid tc0038
Длина 4, Tag 11, Tid t10001d
Длина 4, Tag 11, Tid t10001e
Заметьте, что порядок приема сообщений недетерминирован. Поскольку главный цикл предка обрабатывает сообщения по принципу ``первый пришел - первый вышел'', порядок вывода на экран определяется временем, которое сообщения затрачивают на путешествие от задачи-потомка к предку.