Лаборатория Параллельных информационных технологий НИВЦ МГУ
Опубликован: 22.04.2008 | Доступ: свободный | Студентов: 1177 / 413 | Оценка: 4.30 / 4.19 | Длительность: 08:05:00
Специальности: Программист
Лекция 3:

Передача/прием сообщений между отдельными процессами

< Лекция 2 || Лекция 3: 123 || Лекция 4 >
Аннотация: Практически все программы, написанные с использованием коммуникационной технологии MPI, должны содержать средства для взаимодействия запущенных процессов между собой, которое осуществляется посредством явной посылки сообщений. Об этом подробнее поговорим в этой лекции

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

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

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

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

Передача/прием сообщений с блокировкой

MPI_SEND(BUF, COUNT, DATATYPE, DEST, MSGTAG, COMM, IERR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, DEST, MSGTAG, COMM, IERR

Блокирующая посылка массива BUF с идентификатором MSGTAG, состоящего из COUNT элементов типа DATATYPE, процессу с номером DEST в коммуникаторе COMM. Все элементы посылаемого сообщения должны быть расположены подряд в буфере BUF. Операция начинается независимо от того, была ли инициализирована соответствующая процедура приема. При этом сообщение может быть скопировано как непосредственно в буфер приема, так и помещено в некоторый системный буфер (если это предусмотрено в MPI ). Значение COUNT может быть нулем. Процессу разрешается передавать сообщение самому себе, однако это небезопасно и может привести к возникновению тупиковой ситуации. Параметр DATATYPE имеет в языке Фортран тип INTEGER (в языке Си - предопределенный тип MPi_Datatype ). Тип передаваемых элементов должен указываться с помощью предопределенных констант типа, перечисленных для языка Фортран в следующей таблице.

Таблица 3.1.
Тип данных в MPI Тип данных в Фортране
MPI_INTEGER INTEGER
MPI_REAL REAL
MPI_DOUBLE_PRECISION DOUBLE PRECISION
MPI_COMPLEX COMPLEX
MPI_LOGICAL LOGICAL
MPI CHARACTER CHARACTER (1)
MPI BYTE 8 бит, используется для передачи нетипизированных данных
MPI PACKED тип для упакованных данных

Если используемый с MPI базовый язык имеет дополнительные типы данных, то соответствующие типы должны быть обеспечены и в MPI. Полный список предопределенных имен типов данных перечислен в файле mpif .h (mpi.h).

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

call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr) call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr) next = rank+1
if(next .eq. size) next = MPI_PROC_NULL call MPI_SEND(buf, 1, MPI_REAL, next, & 5, MPI_COMM_WORLD, ierr)

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

Блокировка гарантирует корректность повторного использования всех параметров после возврата из процедуры. Это означает, что после возврата из MPI_SEND можно использовать любые присутствующие в вызове данной процедуры переменные без опасения испортить передаваемое сообщение. Выбор способа осуществления этой гарантии: копирование в промежуточный буфер или непосредственная передача процессу DEST, остается за разработчиками конкретной реализации MPI.

Следует специально отметить, что возврат из процедуры MPI_SEND не означает ни того, что сообщение получено процессом DEST, ни того, что сообщение покинуло процессорный элемент, на котором выполняется процесс, выполнивший данный вызов. Предоставляется только гарантия безопасного изменения переменных, использованных в вызове данной процедуры. Подобная неопределенность далеко не всегда устраивает пользователя. Чтобы расширить возможности передачи сообщений, в MPI введены дополнительные три процедуры. Все параметры у этих процедур такие же, как и у MPI_SEND, однако у каждой из них есть своя особенность.

MPI предоставляет следующие модификации процедуры передачи данных с блокировкой MPI_SEND:

  • MPI_BSEND -передача сообщения с буферизацией. Если прием посылаемого сообщения еще не был инициализирован процессом-получателем, то сообщение будет записано в специальный буфер, и произойдет немедленный возврат из процедуры. Выполнение данной процедуры никак не зависит от соответствующего вызова процедуры приема сообщения. Тем не менее, процедура может вернуть код ошибки, если места под буфер недостаточно. О выделении массива для буферизации должен заботиться пользователь.
  • MPI_SSEND - передача сообщения с синхронизацией. Выход из данной процедуры произойдет только тогда, когда прием посылаемого сообщения будет инициализирован процессом-получателем. Таким образом, завершение передачи с синхронизацией говорит не только о возможности повторного использования буфера посылки, но и о гарантированном достижении процессом-получателем точки приема сообщения в программе. Использование передачи сообщений с синхронизацией может замедлить выполнение программы, но позволяет избежать наличия в системе большого количества не принятых буферизованных сообщений.
  • MPI_RSEND - передача сообщения по готовности. Данной процедурой можно пользоваться только в том случае, если процесс-получатель уже инициировал прием сообщения. В противном случае вызов процедуры, вообще говоря, является ошибочным и результат ее выполнения не определен. Гарантировать инициализацию приема сообщения перед вызовом процедуры MPI_RSEND МОЖНО С ПОМОЩЬЮ операций, осуществляющих явную или неявную синхронизацию процессов (например, MPI_BARRIER ИЛИ MPI_SSEND ). Во многих реализациях процедура MPI_RSEND сокращает протокол взаимодействия между отправителем и получателем, уменьшая накладные расходы на организацию передачи данных.

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

MPI_BUFFER_ATTACH(BUF, SIZE, IERR) <type> BUF(*) INTEGER SIZE, IERR

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

MPI_BUFFER_DETACH(BUF, SIZE, IERR) <type> BUF(*) INTEGER SIZE, IERR

Освобождение выделенного буферного массива для его использования в других целях. Процедура возвращает в аргументах BUF И SIZE адрес и размер освобождаемого массива. Вызвавший процедуру процесс блокируется до того момента, когда все сообщения уйдут из данного буфера.

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

В следующем примере показано использование передачи сообщения с буферизацией. Для буферизации выделяется массив buf, после завершения пересылки он освобождается. Размер необходимого буфера определяется размером сообщения (одно целое число - 4 байта) плюс значение константы MPI_BSEND_OVERHEAD.

program example4
include 'mpif.h'
integer BUFSIZE
parameter (BUFSIZE = 4 + MPI_BSEND_OVERHEAD)
byte buf(BUFSIZE)
integer rank, ierr, ibufsize, rbuf
integer status(MPI_STATUS_SIZE)
call MPI_INIT(ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
if(rank .eq. 0) then
call MPI_BUFFER_ATTACH(buf, BUFSIZE, ierr)
call MPI_BSEND(rank, 1, MPI_INTEGER, 1, 5,&MPI_COMM_WORLD, ierr)
call MPI_BUFFER_DETACH(buf, ibufsize, ierr) end if if(rank .eq. 1) then
call MPI_RECV(rbuf, 1, MPI_INTEGER, 0, 5,&MPI_COMM_WORLD, status, ierr)print *, 
 'Process 1 received ', rbuf, ' from process ',&status(MPI_SOURCE)
end if
call MPI_FINALIZE(ierr) 
end
MPI_RECV(BUF, COUNT, DATATYPE, SOURCE, MSGTAG, COMM, STATUS,
IERR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, SOURCE, MSGTAG, COMM, IERR,
STATUS(MPI_STATUS_SIZE)

Блокирующий прием в буфер BUF не более COUNT элементов сообщения типа DATATYPE с идентификатором MSGTAG от процесса с номером SOURCE в коммуникаторе сомм с заполнением массива атрибутов приходящего сообщения STATUS. Если число реально принятых элементов меньше значения COUNT, то гарантируется, что в буфере BUF изменятся только элементы, соответствующие элементам принятого сообщения. Если количество элементов в принимаемом сообщении больше значения COUNT, TO возникает ошибка переполнения. Чтобы избежать этого, можно сначала определить структуру приходящего сообщения при помощи процедуры MPI_PROBE ( MPI_IPROBE ). ЕСЛИ нужно узнать точное число элементов в принимаемом сообщении, то можно воспользоваться процедурой MPI_GET_COUNT. Блокировка гарантирует, что после возврата из процедуры MPI_RECV все элементы сообщения уже будут приняты и расположены в буфере BUF.

Ниже приведен пример программы, в которой нулевой процесс посылает сообщение процессу с номером один и ждет от него ответа. Если программа будет запущена с большим числом процессов, то реально выполнять пересылки все равно станут только нулевой и первый процессы. Остальные процессы после их инициализации процедурой MPI_INIT напечатают начальные значения переменных а и b, после чего завершатся, выполнив процедуру MPI_FINALIZE.

program example5 include 'mpif.h' integer ierr, size, rank real a, b
integer status(MPI_STATUS_SIZE) call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr) 
  call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr) a = 0.0 b = 0.0
if(rank .eq. 0) then b = 1.0
call MPI_SEND(b, 1, MPI_REAL, 1, 5,& MPI_COMM_WORLD, ierr);
call MPI_RECV(a, 1, MPI_REAL, 1, 5,& MPI_COMM_WORLD, status, ierr);
else
if(rank .eq. 1) then a = 2.0
call MPI_RECV(b, 1, MPI_REAL, 0, 5,& MPI_COMM_WORLD, status, ierr);
call MPI_SEND(a, 1, MPI_REAL, 0, 5,& MPI_COMM_WORLD, ierr);
end if end if
print *, 'process ', rank,' a = ', a, ', b = ', b call MPI_FINALIZE(ierr) 
end

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

program example6 include 'mpif.h' integer ierr, size, rank, 
 a, b integer status(MPI_STATUS_SIZE) call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr) 
 call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr) a = rank b = -1
if(mod(rank, 2) .eq. 0) then if(rank+1 .lt. size) 
 then С посылают все процессы, кроме последнего
call MPI_Send(a, 1, MPI_INTEGER, rank+1, 5,& MPI_COMM_WORLD, ierr);
end if else
call MPI_Recv(b, 1, MPI_INTEGER, rank-1, 5,&MPI_COMM_WORLD, status, ierr);
end if
print *, 'process ', rank,' a = ', a, ', b = ', b call MPI_FINALIZE(ierr) 
end

При приеме сообщения вместо аргументов SOURCE И MSGTAG можно использовать следующие предопределенные константы:

  • MPI_ANY_SOURCE - признак того, что подходит сообщение от любого процесса;
  • MPI_ANY_TAG - признак того, что подходит сообщение с любым идентификатором.

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

Реальные атрибуты принятого сообщения всегда можно определить по соответствующим элементам массива status. В Фортране параметр status является целочисленным массивом размера MPI_STATUS_SIZE. Константы MPI_SOURCE, MPI_TAG и MPI_ERROR являются индексами по данному массиву для доступа к значениям соответствующих полей:

  • status (MPI_SOURCE) -номер процесса-отправителя сообщения;
  • status (MPITAG) -идентификатор сообщения;
  • status (MPI_ERROR) - код ошибки.

В языке Си параметр status является структурой предопределенного типа

MPI_Status С ПОЛЯМИ MPI_SOURCE, MPI_TAG И MPI_ERROR.

Обратим внимание на некоторую несимметричность операций посылки и приема сообщений. С помощью константы MPI_ANY_SOURCE МОЖНО принять сообщение от любого процесса. Однако в случае посылки данных требуется явно указать номер принимающего процесса.

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

MPI_GET_COUNT(STATUS, DATATYPE, COUNT, IERR)
INTEGER COUNT, DATATYPE, IERR, STATUS(MPI_STATUS_SIZE)

По значению параметра STATUS процедура определяет число COUNT уже принятых (после обращения к MPI_RECV ) ИЛИ принимаемых (после обращения к MPI_PROBE или MPI_IPROBE ) элементов сообщения типа DATATYPE. Данная процедура, в частности, необходима для определения размера области памяти, выделяемой для хранения принимаемого сообщения.

MPI_PROBE(SOURCE, MSGTAG, COMM, STATUS, IERR)
INTEGER SOURCE, MSGTAG, COMM, IERR, STATUS(MPI_STATUS_SIZE)

Получение в массиве STATUS информации о структуре ожидаемого сообщения с идентификатором MSGTAG от процесса с номером SOURCE В коммуникаторе сомм с блокировкой. Возврата из процедуры не произойдет до тех пор, пока сообщение с подходящим идентификатором и номером процесса-отправителя не будет доступно для получения. Следует особо обратить внимание на то, что процедура определяет только факт прихода сообщения, но реально его не принимает. Если после вызова MPI_PROBE вызывается MPI_RECV С такими же параметрами, то будет принято то же самое сообщение, информация о котором была получена с помощью вызова процедуры MPI_PROBE.

Следующий пример демонстрирует применение процедуры MPI_PROBE для определения структуры приходящего сообщения. Процесс 0 ждет сообщения от любого из процессов 1 и 2 с одним и тем же тегом. Однако посылаемые этими процессами данные имеют разный тип. Для того чтобы определить, в какую переменную помещать приходящее сообщение, процесс сначала при помощи вызова MPI_PROBE определяет, от кого же именно поступило это сообщение. Следующий непосредственно после MPI_PROBE ВЫЗОВ MPI_RECV гарантированно примет нужное сообщение, после чего принимается сообщение от другого процесса.

program example7
include 'mpif.h'
integer rank, ierr, ibuf, status(MPI_STATUS_SIZE)
real rbuf
call MPI_INIT(ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
ibuf = rank
rbuf = 1.0 * rank
if(rank .eq. 1) call MPI_SEND(ibuf, 1, MPI_INTEGER, 0, 5,& MPI_COMM_WORLD, ierr)
if(rank .eq. 2) call MPI_SEND(rbuf, 1, MPI_REAL, 0, 5,& MPI_COMM_WORLD, ierr)
if(rank .eq. 0) then
call MPI_PROBE(MPI_ANY_SOURCE, 5, MPI_COMM_WORLD,&status, ierr)
if(status(MPI_SOURCE) .EQ. 1) then
call MPI_RECV(ibuf, 1, MPI_INTEGER, 1, 5,& MPI_COMM_WORLD, status, ierr)
call MPI_RECV(rbuf, 1, MPI_REAL, 2, 5,& MPI_COMM_WORLD, status, ierr)
else
if(status(MPI_SOURCE) .EQ. 2) then
call MPI_RECV(rbuf, 1, MPI_REAL, 2, 5,& MPI_COMM_WORLD, status, ierr)
call MPI_RECV(ibuf, 1, MPI_INTEGER, 1, 5,& MPI_COMM_WORLD, status, ierr)
end if end if
print *, 'Process 0 recv ', ibuf, ' from process 1, ',& rbuf, ' from process 2'
end if
call MPI_FINALIZE(ierr) 
end

В следующем примере моделируется последовательный обмен сообщениями между двумя процессами, замеряется время на одну итерацию обмена, определяется зависимость времени обмена от длины сообщения. Таким образом, определяются базовые характеристики коммуникационной сети параллельного компьютера: латентность (время на передачу сообщения нулевой длины) и максимально достижимая пропускная способность (количество мегабайт в секунду) коммуникационной сети, а также длина сообщений, на которой она достигается. Константа NMAX задает ограничение на максимальную длину посылаемого сообщения, а константа NTIMES определяет количество повторений для усреднения результата. Сначала посылается сообщение нулевой длины для определения латентности, затем длина сообщений удваивается, начиная с посылки одного элемента типа real*8.

program example8
include 'mpif.h'
integer ierr, rank, size, i, n, lmax, NMAX, NTIMES
parameter (NMAX = 1 000 000, NTIMES = 10)
double precision time_start, time, bandwidth, max
real*8 a(NMAX)
integer status(MPI_STATUS_SIZE)
call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
time_start = MPI_WTIME(ierr)
n = 0
max = 0.0
lmax = 0
do while(n .le. NMAX)
time_start = MPI_WTIME(ierr)
do i = 1, NTIMES
if(rank .eq. 0) then
call MPI_SEND(a, n, MPI_DOUBLE_PRECISION, 1, 1,& MPI_COMM_WORLD, ierr)
call MPI_RECV(a, n, MPI_DOUBLE_PRECISION, 1, 1,& MPI_COMM_WORLD, status, ierr)
end if if(rank .eq. 1) then
call MPI_RECV(a, n, MPI_DOUBLE_PRECISION, 0, 1,& MPI_COMM_WORLD, status, ierr)
call MPI_SEND(a, n, MPI_DOUBLE_PRECISION, 0, 1,& MPI_COMM_WORLD, ierr)
end if enddo
time = (MPI_WTIME(ierr)-time_start)/2/NTIMES bandwidth = (8*n*1 .d0/(2**20))/time
 if(max .lt. bandwidth) then max = bandwidth lmax = 8*n end if
if(rank .eq. 0) then if(n .eq. 0) then
print *, 'latency = ', time, ' seconds' else
print *, 8*n, ' bytes, bandwidth =', bandwidth, &' Mb/s'
end if end if if(n .eq. 0) then
n = 1 else
n = 2*n end if end do if(rank .eq. 0) then
print *, 'max bandwidth =', max, ' Mb/s , length =',& lmax, ' bytes'
end if
call MPI_FINALIZE(ierr) 
end
Листинг 3.1.
< Лекция 2 || Лекция 3: 123 || Лекция 4 >