4.18сообщения и их атрибуты.Каждое сообщение MPI обладает следующим набором атрибутов: Номер ветви-отправителя Номер или номера ветвей-получателей Коммуникатор, т.е. описатель коммуникационного контекста, в котором данное сообщение передается Тип данных, образующих тело сообщения Количество элементов указанного типа, составляющих тело сообщения Тэг, или бирка сообщения В MPI не существует специальной структуры данных, описывающей сообщение. Вместо этого, атрибуты сообщения передаются в соответствующие функции и получаются из них как отдельные параметры. Тэг сообщения представляет собой целое число, аналог типа сообщения в механизме очередей сообщений IPC. Интерпретация этого атрибута целиком возлагается на саму программу. Поддержка типов данных в MPI. В отличие от других средств межпроцессного взаимодействия, рассмотренных нами ранее, в MPI требуется явное указание типа данных, образующих тело сообщения. Причина этого заключается в том, что библиотека MPI берет на себя заботу о корректности передачи данных в случае использования ее в гетерогенной (т.е. состоящей из разнородных узлов) среде. Одни и те же типы данных на машинах разных архитектур могут иметь различное представление (к примеру, в архитектуре Intel представление целых чисел характеризуется расположением байт от младшего к старшему, в то время как в процессорах Motorola принято обратное расположение; кроме того, на разных машинах один и тот же тип может иметь разную разрядность и выравнивание). Очевидно, что в такой ситуации передача данных как последовательности байт без соответствующих преобразований приведет к их неверной интерпретации. Реализация MPI производит необходимые преобразования данных в теле сообщения перед его посылкой и после получения, однако для этого необходима информация о типе данных, образующих тело сообщения. Для описания типов данных в MPI введен специальный тип – MPI_Datatype. Для каждого стандартного типа данных Си в заголовочном файле библиотеки описана константа типа MPI_Datatype; ее имя состоит из префикса MPI_ и имени типа, написанного большими буквами, например, MPI_INT, MPI_DOUBLE и т.д. Кроме этого, MPI предоставляет широкие возможности по конструированию описателей производных типов, однако их рассмотрение выходит за рамки данного пособия. Параметр типа MPI_Datatype, описывающий тип данных, образующих тело сообщения, присутствует во всех функциях приема и передачи сообщений MPI. Непосредственно с поддержкой типов данных MPI связана еще одна особенность функций приема-передачи данных: в качестве размера тела сообщения всюду фигурирует не количество байт, а количество элементов указанного типа. Коммуникации «точка-точка». Блокирующий режим. Библиотека MPI предоставляет возможности для организации как индивидуального обмена сообщениями между парой ветвей – в этом случае у каждого сообщения имеется единственный получатель – так и коллективных коммуникаций, в которых участвуют все ветви, входящие в определенный коммуникационный контекст. Рассмотрим сначала функции MPI, используемые для отправки и получения сообщений между двумя ветвями – такой способ обмена в литературе часто носит название коммуникации «точка-точка». Библиотека MPI предоставляет отдельные пары функций для реализации блокирующих и неблокирующих операций приема и посылки данных. Заметим, что они совместимы друг с другом в любых комбинациях, например, для отправки сообщения может использоваться блокирующая операция, а для приема этого же сообщения – неблокирующая, и наоборот. Отправка сообщений в блокирующем режиме. Для отправки сообщений в блокирующем режиме служит функция MPI_Send(): #include <mpi.h> int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm); Аргументы у этой функции следующие: buf – указатель на буфер, в котором расположены данные для передачи count – количество элементов заданного типа в буфере datatype – тип элементов в буфере dest – уникальный номер ветви-получателя. Представляет собой неотрицательное целое число в диапазоне [0, N-1], где N – число ветвей в коммуникационном контексте, описываемом коммуникатором comm tag – тэг (бирка) сообщения. Представляет собой неотрицательное целое число от 0 до некоторого максимума, значение которого зависит от реализации (однако стандарт MPI гарантирует, что этот максимум имеет значение не менее 32767). Тэги сообщений могут использоваться ветвями для того, чтобы отличать разные по смыслу сообщения друг от друга. comm – коммуникатор, описывающий коммуникационный контекст, в котором будет передаваться сообщение Еще раз отметим, что в этой функции, как и во всех остальных функциях приема-передачи MPI, в параметре count указывается не длина буфера в байтах, а количество элементов типа datatype, образующих буфер. При вызове MPI_Send() управление возвращается процессу только тогда, когда вызвавший процесс может повторно использовать буфер buf (т.е. записывать туда другие данные) без риска испортить еще не переданное сообщение. Однако, возврат из функции MPI_Send() не обязательно означает, что сообщение было доставлено адресату – в реализации данной функции может использоваться буферизация, т.е. тело сообщения может быть скопировано в системный буфер, после чего происходит возврат из MPI_Send(), а сама передача будет происходить позднее в асинхронном режиме. С одной стороны, буферизация при отправке позволяет ветви-отправителю сообщения продолжить выполнение, не дожидаясь момента, когда ветвь-адресат сообщения инициирует его прием; с другой стороны – буферизация, очевидно, увеличивает накладные расходы как по памяти, так и по времени, так как требует добавочного копирования. При использовании вызова MPI_Send() решение о том, будет ли применяться буферизация в каждом конкретном случае, принимается самой библиотекой MPI. Это решение принимается из соображений наилучшей производительности, и может зависеть от размера сообщения, а также от наличия свободного места в системном буфере. Если буферизация не будет применяться, то возврат управления из MPI_Send() произойдет только тогда, когда ветвь-адресат инициирует прием сообщения, и его тело будет скопировано в буфер-приемник ветви-адресата. Режимы буферизации. MPI позволяет программисту самому управлять режимом буферизации при отправке сообщений в блокирующем режиме. Для этого в MPI существуют три дополнительные модификации функции MPI_Send(): MPI_Bsend(), MPI_Ssend() и MPI_Rsend(): #include <mpi.h> int MPI_Bsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm); int MPI_Ssend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm); int MPI_Rsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm); Их параметры полностью аналогичны MPI_Send(), различие заключается только в режимах буферизации. MPI_Bsend() предполагает отсылку сообщения с буферизацией. Это означает, что если к моменту вызова MPI_Bsend() ветвь-адресат не инициировала прием сообщения, и т.о. сообщение не может быть доставлено немедленно, оно должно быть буферизовано. Если в буфере окажется недостаточно свободного места, MPI_Bsend() вернет ошибку. MPI предоставляет специальные функции, с помощью которых ветвь может выделить в своем адресном пространстве некоторый буфер, который будет использоваться MPI для буферизации сообщений в этом режиме, таким образом, программист может сам управлять размером доступного буферного пространства. Заметим, что посылка сообщения с помощью функции MPI_Bsend() является, в отличие от всех остальных операций отсылки, локальной операцией, т.е. ее завершение не зависит от поведения ветви-адресата. Функция MPI_Ssend(), наоборот, представляет собой отсылку сообщения без буферизации. Возврат управления из функции MPI_Ssend() осуществляется только тогда, когда адресат инициировал прием сообщения, и оно скопировано в буфер-приемник адресата. Если на принимающей стороне используется блокирующая функция приема сообщения, то такая коммуникация представляет собой реализацию схемы рандеву: операция обмена, инициированная на любой из сторон, не завершится до тех пор, пока вторая сторона также не притупит к обмену. Вызов функции MPI_Rsend() сообщает MPI о том, что ветвь-адресат уже инициировала запрос на прием сообщения. В некоторых случаях знание этого факта позволяет MPI использовать более короткий протокол для установления соединения и тем самым повысить производительность передачи данных. Однако, использование этой функции ограничено теми ситуациями, когда ветви-отправителю известно о том, что ветвь-адресат уже ожидает сообщение. В случае, если на самом деле ветвь-адресат не инициировала прием сообщения, MPI_Rsend() завершается с ошибкой. Прием сообщений в блокирующем режиме. Прием сообщений в блокирующем режиме осуществляется функцией MPI_Recv(): #include <mpi.h> int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status); Она имеет следующие аргументы: buf – указатель на буфер, в котором нужно разместить тело принимаемого сообщения count – максимальное количество элементов заданного типа, которое может уместиться в буфере datatype – тип элементов в теле получаемого сообщения dest – уникальный номер ветви-отправителя. С помощью этого параметра процесс получатель может указать конкретного отправителя, от которого он желает получить сообщение, либо сообщить о том, что он хочет получить сообщение от любой ветви данного коммуникатора – в этом случае в качестве значения параметра указывается константа MPI_ANY_SOURCE tag – тэг (бирка) сообщения. Ветвь-получатель может инициировать прием сообщения с конкретным значением тэга, либо указать в этом параметре константу MPI_ANY_TAG для получения сообщения с любым значением тэга. comm – коммуникатор, описывающий коммуникационный контекст, в котором принимается сообщение status – указатель на структуру типа MPI_Status, поля которой будут заполнены функцией MPI_Recv(). Используя эту структуру, ветвь-получатель может узнать дополнительную информацию о сообщении, такую как, например, его фактический размер, а также фактические значения тэга и отправителя в том случае, если использовались константы MPI_ANY_SOURCE и MPI_ANY_TAG. Если в момент вызова MPI_Recv() нет ни одного сообщения, удовлетворяющего заданным критериям (т.е. посланного через заданный коммуникатор и имеющего нужного отправителя и тэг, в случае, если были заданы их конкретные значения), то выполнение ветви блокируется до момента поступления такого сообщения. В любом случае, управление возвращается процессу лишь тогда, когда сообщение будет получено, и его данные будут записаны в буфер buf и структуру status. Отметим, что фактический размер принятого сообщения может быть меньше, чем указанная в параметре count максимальная емкость буфера. В этом случае сообщение будет успешно получено, его тело записано в буфер, а остаток буфера останется нетронутым. Для того, чтобы узнать фактический размер принятого сообщения, служит функция MPI_Get_count(): #include <mpi.h> int MPI_Get_count(MPI_Status *status, MPI_Datatype datatype, int *count); Этой функции передаются в качестве параметров указатель на структуру типа MPI_Status, которая была заполнена в момент вызова функции приема сообщения, а также тип данных, образующих тело принятого сообщения. Параметр count представляет собой указатель на переменную целого типа, в которую будет записано количество элементов типа datatype, образующих тело принятого сообщения. Если же при приеме сообщения его размер окажется больше указанной максимальной емкости буфера, функция MPI_Recv() вернет соответствующую ошибку. Для того чтобы избежать такой ситуации, можно сначала вызвать функцию MPI_Probe(), которая позволяет получить информацию о сообщении, не принимая его: #include <mpi.h> int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status *status); Параметры этой функции аналогичны последним 4-м параметрам функции MPI_Recv(). Функция MPI_Probe(), как и MPI_Recv(), возвращает управление только при появлении сообщения, удовлетворяющего переданным параметрам, и заполняет информацией о сообщении структуру типа MPI_Status, указатель на которую передается в последнем параметре. Однако, в отличие от MPI_Recv(), вызов MPI_Probe() не осуществляет собственно прием сообщения. После возврата из MPI_Probe() сообщение остается во входящей очереди и может быть впоследствии получено одним из вызовов приема данных, в частности, MPI_Recv(). MPI: прием сообщения, размер которого неизвестен заранее. В данном примере приведена схема работы с сообщением, размер которого заранее неизвестен. Ветвь с номером 0 посылает ветви с номером 1 сообщение, содержащее имя исполняемой программы (т.е. значение нулевого элемента в массиве аргументов командной строки). В ветви с номером 1 вызов MPI_Probe() позволяет получить доступ к описывающей сообщение структуре типа MPI_Status. Затем с помощью вызова MPI_Get_count() можно узнать размер буфера, который необходимо выделить для приема сообщения. #include <mpi.h> #include <stdio.h>
int main(int argc, char **argv) { int size, rank;
MPI_Init(&argc, &argv); /* Инициализируем библиотеку */ MPI_Comm_size(MPI_COMM_WORLD, &size); /* Узнаем количество задач в запущенном приложении... */ MPI_Comm_rank (MPI_COMM_WORLD, &rank); /* ...и свой собственный номер: от 0 до (size-1) */
if ((size > 1) && (rank == 0)) { /* задача с номером 0 отправляет сообщение*/ MPI_Send(argv[0], strlen(argv[0]), MPI_CHAR, 1, 1, MPI_COMM_WORLD); printf("Sent to process 1: \"\%s\" ", argv[0]); } else if ((size > 1) && (rank == 1)) { /* задача с номером 1 получает сообщение*/ int count; MPI_Status status; char *buf;
MPI_Probe(0, 1, MPI_COMM_WORLD, &status); MPI_Get_count(&status, MPI_CHAR, &count); buf = (char *) malloc(count * sizeof(char)); MPI_Recv(buf, count, MPI_CHAR, 0, 1, MPI_COMM_WORLD, &status); printf("Received from process 0: \"\%s\" ", buf); }
/* Все задачи завершают выполнение */ MPI_Finalize(); return 0; } Коммуникации «точка-точка». Неблокирующий режим. Во многих приложениях для повышения производительности программы бывает выгодно совместить отправку или прием сообщений с основной вычислительной работой, что удается при использовании неблокирующего режим приема и отправки сообщений. В этом режиме вызов функций приема либо отправки сообщения инициирует начало операции приема/передачи и сразу после этого возвращает управление вызвавшей ветви, а сам процесс передачи данных происходит асинхронно. Однако, для того, чтобы инициировавшая операцию приема/передачи ветвь имела возможность узнать о том, что операция завершена, необходимо каким-то образом идентифицировать каждую операцию обмена. Для этого в MPI существует механизм так называемых «квитанций» (requests), для описания которых служит тип данных MPI_Request. В момент вызова функции, инициирующей операцию приема/передачи, создается квитанция, соответствующая данной операции, и через параметр-указатель возвращается вызвавшей ветви. Впоследствии, используя эту квитанцию, ветвь может узнать о завершении ранее начатой операции. Отсылка и прием сообщений в неблокирующем режиме. Для отсылки и приема сообщений в неблокирующем режиме служат следующие базовые функции: #include <mpi.h> int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request); int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request); Все их параметры, кроме последнего, аналогичны параметрам функций MPI_Send() и MPI_Recv(). Последним параметром служит указатель на переменную типа MPI_Request, которая после возврата из данного вызова будет содержать квитанцию для инициированной операции отсылки или приема данных. Возврат из функции MPI_Isend() происходит сразу же, как только система инициирует операцию отсылки сообщения, и будет готова начать копировать данные из буфера, переданного в параметре buf. Важно понимать, что возврат из функции MPI_Isend() не означает, что этот буфер можно использовать по другому назначению! Вызвавший процесс не должен производить никаких операций над этим буфером до тех пор, пока не убедится в том, что операция отсылки данных завершена. Возврат из функции MPI_Irecv() происходит сразу же, как только система инициирует операцию приема данных и будет готова начать принимать данные в буфер, предоставленный в параметре buf, вне зависимости от того, существует ли в этот момент сообщение, удовлетворяющее остальным переданным параметрам (source, tag, comm). При этом вызвавший процесс не может полагать, что в буфере buf находятся принятые данные до тех пор, пока не убедится, что операция приема завершена. Отметим также, что у функции MPI_Irecv() отсутствует параметр status, так как на момент возврата из этой функции информация о принимаемом сообщении, которая должна содержаться в status, может быть еще неизвестна. В неблокирующем режиме, также как и в блокирующем, доступны различные варианты буферизации при отправке сообщения. Для явного задания режима буферизации, отличного от стандартного, служат следующие функции: #include <mpi.h> int MPI_Issend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request); int MPI_Ibsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request); int MPI_Irsend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request); Отметим, что поскольку MPI_Ibsend() предполагает обязательную буферизацию сообщения, а MPI_rsend() используется лишь тогда, когда на принимающей стороне уже гарантированно инициирован прием сообщения, их использование лишь в некоторых случаях может дать сколь либо заметный выигрыш по сравнению с аналогичными блокирующими вариантами, так как разницу во времени их выполнения составляет лишь время, необходимое для копирования данных. Работа с квитанциями. Библиотека MPI предоставляет целое семейство функций, с помощью которых процесс, инициировавший одну или несколько асинхронных операций приема/передачи сообщений, может узнать о статусе их выполнения. Самыми простыми из этих функций являются MPI_Wait() и MPI_Test(): #include <mpi.h> int MPI_Wait(MPI_Request *request, MPI_Status *status); int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status); Функция MPI_Wait() вернет управление лишь тогда, когда операция приема/передачи сообщения, соответствующая квитанции, переданной в параметре request, будет завершена (для операции отсылки, инициированной функцией MPI_Ibsend() это означает, что данные были скопированы в специально отведенный буфер). При этом для операции приема сообщения в структуре, на которую указывает параметр status, возвращается информация о полученном сообщении, аналогично тому, как это делается в MPI_Recv(). После возврата из функции MPI_Wait() для операции отсылки сообщения процесс может повторно использовать буфер, содержавший тело отосланного сообщения, а для операции приема сообщения гарантируется, что после возврата из MPI_Wait() в буфере для приема находится тело принятого сообщения. Для того, чтобы узнать, завершена ли та или иная операция приема/передачи сообщения, но избежать блокирования в том случае, если операция еще не завершилась, служит функция MPI_Test(). В параметре flag ей передается адрес целочисленной переменной, в которой будет возвращено ненулевое значение, если операция завершилась, и нулевое в противном случае. В случае, если квитанция соответствовала операции приема сообщения, и данная операция завершилась, MPI_Test() заполняет структуру, на которую указывает параметр status, информацией о принятом сообщении. Для того, чтобы узнать статус сразу нескольких асинхронных операций приема/передачи сообщений, служит ряд функций работы с массивом квитанций: #include <mpi.h> int MPI_Waitany(int count, MPI_Request *array_of_requests, int *index, MPI_Status *status); int MPI_Testany(int count, MPI_Request *array_of_requests, int *index, int *flag, MPI_Status *status); int MPI_Waitsome(int count, MPI_Request *array_of_requests, int *outcount, int *array_of_indices, MPI_Status *array_of_statuses); int MPI_Testsome(int count, MPI_Request *array_of_requests, int *outcount, int *array_of_indices, MPI_Status *array_of_statuses); int MPI_Waitall(int count, MPI_Request *array_of_requests, MPI_Status *array_of_statuses); int MPI_Testall(int count, MPI_Request *array_of_requests, int *flag, MPI_Status *array_of_statuses); Все эти функции получают массив квитанций в параметре array_of_requests и его длину в параметре count. Функция MPI_Waitany() блокируется до тех пор, пока не завершится хотя бы одна из операций, описываемых переданными квитанциями. Она возвращает в параметре index индекс квитанции для завершившейся операции в массиве array_of_requests и, в случае, если завершилась операция приема сообщения, заполняет структуру, на которую указывает параметр status, информацией о полученном сообщении. MPI_Testany() не блокируется, а возвращает в переменной, на которую указывает параметр flag, ненулевое значение, если одна из операций завершилась. В этом случае она возвращает в параметрах index и status то же самое, что и MPI_Waitany(). Отметим, что если несколько операций из интересующего множества завершаются одновременно, то и MPI_Waitany(), и MPI_Testany() возвращают индекс и статус лишь одной из них, выбранной случайным образом. Более эффективным вариантом в таком случае является использование соответственно MPI_Waitsome() и MPI_Testsome(): их поведение аналогично MPI_Waitany() и MPI_Testany() за тем исключением, что в случае, если одновременно завершается несколько операций, они возвращают статус для всех завершившихся операций. При этом в параметре outcount возвращается количество завершившихся операций, в параметре array_of_indices – индексы квитанций завершившихся операций в исходном массиве квитанций, а массив array_of_statuses содержит структуры типа MPI_Status, описывающие принятые сообщения (значения в массиве array_of_statuses имеют смысл только для операций приема сообщения). Отметим, что у функции MPI_Testany() нет параметра flag – вместо этого, она возвращает 0 в параметре outcount, если ни одна из операций не завершилась. Функция MPI_Waitall() блокируется до тех пор, пока не завершатся все операции, квитанции для которых были ей переданы, и заполняет информацию о принятых сообщениях для операций приема сообщения в элементах массива array_of_statuses (при этом i-й элемент массива array_of_statuses соответствует операции, квитанция для которой была передана в i-м элементе массива array_of_requests). Функция MPI_Testall() возвращает ненулевое значение в переменной, адрес которой указан в параметре flag, если завершились все операции квитанции для которых были ей переданы, и нулевое значение в противном случае. При этом, если все операции завершились, она заполняет элементы массива array_of_statuses аналогично тому, как это делает MPI_Waitall(). |
|