Первым примером демонстрируется, как порождать задачи ПВМ и синхронизировать их. Программа порождает несколько задач - по умолчанию три. Потом потомок синхронизируется путем передачи сообщения своей задаче-предку. Предок принимает сообщения от каждой им порожденной задачи и выводит на экран информацию, заключенную в этих сообщениях от задач-потомков.
Программа "раздваивание-присоединение" содержит код как задачи-предка, так и задачи-потомка. Позвольте исследовать это более подробно. Самое первое действие, которое делает программа - вызов pvm_mytid(). Функция должна вызываться для того, чтобы впоследствии можно было делать остальные вызовы ПВМ. В результате pvm_mytid() всегда должно быть положительное целое число. Если это не верно, то что-то произошло слишком неправильно. В примере "раздваивание-присоединение" проверяется значение mytid; если оно отражает ошибку, то вызывается pvm_perror() и осуществляется выход из программы. Вызов pvm_perror() выведет на экран сообщение, показывающее, что при последнем вызове ПВМ имела место ошибка. В данном случае, последним вызовом был pvm_mytid(), так что pvm_perror может вывести на экран сообщение о том, что ПВМ не смогла запуститься на текущей машине. Аргумент 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_spawn() приказывает ПВМ запустить ntask задач с именами argv[0]. Второй параметр - список аргументов для передачи порождаемым задачам. В данном случае, не нужно заботиться о передаче потомкам частных аргументов через командную строку, поэтому он равен NULL. Третий параметр при порождении - PvmTaskDefault - флаг, приказывающий ПВМ породить задачи в "местах" по умолчанию. Если бы было необходимо размещение потомков на специфических машинах или на машинах с особенной архитектурой, то можно было бы воспользоваться значением флага 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 говорит о том, что ПВМ
должна преобразовать данные обычным способом - чтобы удостовериться,
что они прибыли в формате, корректном для целевого процессора. В некоторых
случаях отсутствует необходимость в преобразовании данных. Если пользователь
уверен, что целевая машина использует тот же формат данных и действительно
преобразовывать данные нет нужды, то он может применить PvmDataRaw
в качестве параметра pvm_initsend(). Вызов pvm_pkint()
размещает единственное целое число - metid - в буфере сообщения.
Важно быть уверенными, что соответствующий распаковочный вызов точно
соответствует данному упаковочному. Если число упаковывается как целое,
а распаковывается, как число с плавающей запятой, то такой подход
не корректен. Аналогично, если пользователь упаковывает два целых
числа одним вызовом, то он не сможет их впоследствии распаковать двойным
вызовом pvm_upkint() - по одному вызову на каждое целое
число. Таким образом, между упаковочным и распаковочным вызовами должно
существовать соответствие "один к одному". В завершение, сообщение
с тегом JOINTAG передается задаче-предку.
Пример "раздваивание-присоединение":
Пример "раздваивание-присоединение"
Демонстрируется, как порождать процессы и обмениваться сообщениями
*/
/* определения и прототипы библиотеки ПВМ */
#include <pvm3.h>
/* максимальное число потомков, которые будут
порождаться этой программой */
#define MAXNCHILD 20
/* тег для использования в сообщениях, связанных с присоединением */
#define JOINTAG 11
int main(int argc, char* argv[])
{
/* количество задач для порождения, 3 используются по умолчанию */
int ntask = 3;
/* код возврата для вызовов ПВМ */
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
На рис. 86. показано, что выводит на экран выполняющаяся программа "раздваивание - присоединание". Заметьте, что порядок приема сообщений недетерминирован. Поскольку главный цикл предка обрабатывает сообщения по принципу "первый пришел - первый вышел", порядок вывода на экран определяется просто временем, которое сообщения затрачивают при путешествии от задачи-потомка к предку.