Молодогвардейцев 454015 Россия, Челябинская область, город Челябинск 89085842764
MindHalls logo

Общение процессов в MPI

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

Набор статей про библиотеку MPI разрастается, поэтому я решил привести список предыдущих «серий». Итак, мы уже умеем:
устанавливать MPI в Linux Ubuntu
настраивать Eclipse для работы с MPI
писать простейшую программу с помощью базовых функций MPI

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

Функции отправки сообщения MPI_Send и MPI_Isend

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

Блокирующая отправка сообщения в MPI

MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);

На вход принимает адрес буфера с сообщением buf, количество данных в буфере count, тип данных datatype, ранг процесса получателя dest, тег сообщения tag, коммуникатор в котором происходит обмен comm, оба процесса(отправитель и получатель) должны находиться в этом коммуникаторе.

Теги нужны для того, чтобы различать посылки, если мы отправим с тегом 0, то и принимать должны нулем. Кроме того, тег не может принимать отрицательное значение. А для случая, когда нам не важно с каким тегом принимать сообщение, существует константа MPI_ANY_TAG.

Неблокирующая отправка сообщения в MPI

MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Request *request);

Аргументы не изменились, но добавился request, в который запишется информация о запросе, в дальнейшем он используется в таких функциях, как MPI_Wait или MPI_Test. О них чуть ниже.

Функции приема сообщения MPI_Recv и MPI_Irecv

Ловить с другой стороны наш буфер будет функция MPI_Recv. Блокирующий вариант не сдвинет программу с места, пока не получит сообщение. Неблокирующий — MPI_Irecv не станет останавливать работу, а попытается поймать сообщение, если пришло, хорошо, не пришло, ну и ладно, работаем дальше. Проверить успех или неудачу получения сообщения можно чуть позже с помощью функций MPI_Wait или MPI_Test.

Блокирующий прием сообщения в MPI

MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status);

На вход принимает адрес буфера buf для полученного сообщения, который должен смочь вместить в себя count элементов типа datatype, ранг процесса источника source, тег сообщения tag, коммуникатор comm. Отправитель и получатель должны находиться в одном коммуникаторе. Последний аргумент status — статус посылки.

Неблокирующий прием сообщения в MPI

MPI_Irecv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request);

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

Функции MPI_Wait и MPI_Test

Пришло время поговорить про функции проверки успеха передачи сообщения. Вот их прототипы.

int MPI_Wait(MPI_Request *request, MPI_Status *status);
int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status);

Тест отличается наличием флага во входных аргументах. Обе функции нужны для того, чтобы заблокировать процесс ровно до окончания передачи(отправки или получения) сообщения, связанного со структурой request. Обе вернут константу MPI_SUCCESS в случае успеха, когда операция передачи завершена. Кроме того, заполнится структура status, по которой тоже можно отследить успех передачи. Функция MPI_Test дополнительно ставит flag = true в случае успеха, и flag = false иначе.

Практический пример обмена сообщениями в MPI

Реализуем топологию «кольцо», в которой каждый процесс сгенерирует свой случайный массив целых чисел и отправит его следующему по рангу. В свою очередь нулевой процесс должен получить сообщение от size-1 процесса. Размер массива приходит аргументом командной строки.

#include <cstdlib>
#include <iostream>
#include <ctime>

#include "mpi.h"

#define TOP_RAND 101

using namespace std;

void randomizeIntArray(int* array, int size) {
    for(int i = 0; i < size; i++) {
        array[i] = rand() % TOP_RAND;
    }

    return;
}

void shiftMsg(int rank, int size, int arraySize) {
    int *arr = (int*)malloc(sizeof(int) * arraySize);
    int *res = (int*)malloc(sizeof(int) * arraySize);

    randomizeIntArray(arr, arraySize);

    MPI_Request request;
    MPI_Isend(arr, arraySize, MPI_INT, (rank+1) % size, 0, MPI_COMM_WORLD, &request);

    cout << "process: " << rank << " sent array: ";
    for(int i = 0; i < arraySize; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    MPI_Irecv(res, arraySize, MPI_INT, (rank-1 + size) % size, 0, MPI_COMM_WORLD, &request);

    MPI_Status status;
    //Проверяем, получил ли процесс сообщение 
    if(MPI_Wait(&request, &status) == MPI_SUCCESS) {
        cout << "process: " << rank << " recv array: ";
        for(int i = 0; i < arraySize; i++) {
            cout << res[i] << " ";
        }
        cout << endl;
    }

    free(arr);
    free(res);

    return;
}

int main(int argc, char** argv) {
    if(argc != 2) {
        printf("Needs arg: array size\n");
        return 0;
    }
    int rank;
    int size;

    int arraySize;
    arraySize = atoi(argv[1]);

    MPI_Init(&argc, &argv);

    MPI_Comm_size(MPI_COMM_WORLD, &size);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);

    //Самый простой способ задания уникального сида для каждого процесса
    srand(time(NULL) + rank);

    if(rank == 0) {
        cout << "Shift func: " << endl;
    }
    shiftMsg(rank, size, arraySize);

    MPI_Finalize();
    return 0;
}

Заключение

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