Опубликован: 22.04.2008 | Уровень: специалист | Доступ: платный | ВУЗ: Лаборатория Параллельных информационных технологий НИВЦ МГУ
Лекция 3:

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

< Лекция 2 || Лекция 3: 123 || Лекция 4 >

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

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

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

Если есть возможность операции приема/передачи сообщений скрыть на фоне вычислений, то этим, вроде бы, надо безоговорочно пользоваться. Однако на практике не все всегда согласуется с теорией. Многое зависит от конкретной реализации. К сожалению, далеко не всегда асинхронные операции эффективно поддерживаются аппаратурой и системным окружением. Поэтому не стоит удивляться, если эффект от выполнения вычислений на фоне пересылок окажется нулевым или совсем небольшим. Сделанные замечания касаются только вопросов эффективности. В отношении предоставляемой функциональности асинхронные операции исключительно полезны, поэтому они присутствуют практически в каждой реальной программе.

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

Неблокирующая посылка из буфера BUF COUNT элементов сообщения типа DATATYPE с идентификатором MSGTAG процессу DEST коммуникатора сомм. Возврат из процедуры происходит сразу после инициализации процесса передачи без ожидания обработки всего сообщения, находящегося в буфере BUF. Это означает, что нельзя повторно использовать данный буфер для других целей без получения дополнительной информации, подтверждающей завершение данной посылки. Определить тот момент времени, когда можно повторно использовать буфер BUF без опасения испортить передаваемое сообщение, можно с помощью возвращаемого параметра REQUEST И процедур семейств MPI_WAIT И MPI_TEST. Параметр REQUEST имеет в языке Фортран тип INTEGER (в языке Си - предопределенный тип MPi_Request ) и используется для идентификации конкретной неблокирующей операции.

Аналогично трем модификациям процедуры MPI_SEND, предусмотрены три дополнительных варианта процедуры MPI_ISEND:

  • MPI_IBSEND - неблокирующая передача сообщения с буферизацией;
  • MPI_ISSEND - неблокирующая передача сообщения с синхронизацией;
  • MPI_IRSEND - неблокирующая передача сообщения по готовности.

К изложенной выше семантике работы этих процедур добавляется отсутствие блокировки.

MPI_IRECV(BUF, COUNT, DATATYPE, SOURCE, MSGTAG, COMM, REQUEST, IERR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, SOURCE, MSGTAG, COMM, REQUEST, IERR

Неблокирующий прием в буфер BUF не более COUNT элементов сообщения типа DATATYPE с идентификатором MSGTAG от процесса с номером SOURCE В коммуникаторе сомм с заполнением массива STATUS. В отличие от блокирующего приема, возврат из процедуры происходит сразу после инициализации процесса приема без ожидания получения всего сообщения и его записи в буфере BUF. Окончание процесса приема можно определить с помощью параметра REQUEST и процедур семейств MPI_WAIT И MPI_TEST.

Сообщение, отправленное любой из процедур MPI_SEND, MPI_ISEND И любой из трех их модификаций, может быть принято любой из процедур MPI_RECV и MPI_IRECV.

Обратим особое внимание на то, что до завершения неблокирующей операции не следует записывать в используемый массив данных!

MPI_IPROBE(SOURCE, MSGTAG, COMM, FLAG, STATUS, IERR)
LOGICAL FLAG
INTEGER SOURCE, MSGTAG, COMM, IERR, STATUS(MPI_STATUS_SIZE)

Получение в массиве STATUS информации о структуре ожидаемого сообщения с идентификатором MSGTAG ОТ процесса с номером SOURCE В коммуникаторе сомм без блокировки. В параметре FLAG возвращается значение .TRUE., если сообщение с подходящими атрибутами уже может быть принято (в этом случае действие процедуры полностью аналогично MPI_PROBE ), И значение .FALSE., если сообщения с указанными атрибутами еще нет.

MPI_WAIT(REQUEST, STATUS, IERR)
INTEGER REQUEST, IERR, STATUS(MPI_STATUS_SIZE)

Ожидание завершения асинхронной операции, ассоциированной с идентификатором REQUEST и запущенной вызовом процедуры MPI_ISEND или MPI_IRECV. Пока асинхронная операция не будет завершена, процесс, выполнивший процедуру MPI_WAIT, будет заблокирован. Для операции неблокирующего приема определяется параметр STATUS. После выполнения процедуры идентификатор неблокирующей операции REQUEST устанавливается в значение MPI_REQUEST_NULL.

MPI_WAITALL(COUNT, REQUESTS, STATUSES, IERR)
INTEGER COUNT, REQUESTS(*), STATUSES (MPI_STATUS_SIZE,*), IERR

Ожидание завершения COUNT асинхронных операций, ассоциированных с идентификаторами массива REQUESTS. ДЛЯ операций неблокирующих приемов определяются соответствующие параметры в массиве STATUSES. Если во время одной или нескольких операций обмена возникли ошибки, то поле ошибки в элементах массива STATUSES будет установлено в соответствующее значение. После выполнения процедуры соответствующие элементы параметра REQUESTS устанавливаются в значение MPI_REQUEST_NULL.

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

program example9
include 'mpif.h'
integer ierr, rank, size, prev, next, reqs(4), buf(2)
integer stats(MPI_STATUS_SIZE, 4)
call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
prev = rank - 1
next = rank + 1
if (rank .eq. 0) prev = size - 1
if (rank .eq. size - 1) next = 0
call MPI_IRECV(buf(1), 1, MPI_INTEGER, prev, 5,& MPI_COMM_WORLD, reqs(1), ierr)
call MPI_IRECV(buf(2), 1, MPI_INTEGER, next, 6,& MPI_COMM_WORLD, reqs(2), ierr)
call MPI_ISEND(rank, 1, MPI_INTEGER, prev, 6,& MPI_COMM_WORLD, reqs(3), ierr)
call MPI_ISEND(rank, 1, MPI_INTEGER, next, 5,& MPI_COMM_WORLD, reqs(4), ierr)
call MPI_WAITALL(4, reqs, stats, ierr);
print *, 'process ', rank, &        ' prev=', buf(1), ' next=', buf(2)
call MPI_FINALIZE(ierr)
end
MPI_WAITANY(COUNT, REQUESTS, INDEX, STATUS, IERR)
INTEGER COUNT, REQUESTS(*), INDEX, STATUS(MPI_STATUS_SIZE), IERR

Ожидание завершения одной из COUNT асинхронных операций, ассоциированных с идентификаторами REQUESTS. ЕСЛИ К моменту вызова завершились несколько из ожидаемых операций, то случайным образом будет выбрана одна из них. Параметр INDEX содержит номер элемента в массиве REQUESTS, содержащего идентификатор завершенной операции. Для неблокирующего приема определяется параметр STATUS. После выполнения процедуры соответствующий элемент параметра REQUESTS устанавливается в значение

MPI_REQUEST_NULL.
MPI_WAITSOME(INCOUNT, REQUESTS, OUTCOUNT, INDEXES, STATUSES, IERR)
INTEGER INCOUNT, REQUESTS(*), OUTCOUNT, INDEXES(*), IERR,
STATUSES(MPI_STATUS_SIZE,*)

Ожидание завершения хотя бы одной из INCOUNT асинхронных операций, ассоциированных с идентификаторами REQUESTS. Параметр OUTCOUNT содержит число завершенных операций, а первые OUTCOUNT элементов массива INDEXES содержат номера элементов массива REQUESTS с их идентификаторами. Первые OUTCOUNT элементов массива STATUSES содержат параметры завершенных операций (для неблокирующих приемов). После выполнения процедуры соответствующие элементы параметра REQUESTS устанавливаются в значение MPI_REQUEST_NULL.

В следующем примере демонстрируется схема использования процедуры MPI_WAITSOME для организации коммуникационной схемы "master-slave" (все процессы общаются с одним выделенным процессом). Все процессы кроме процесса 0 на каждой итерации цикла определяют с помощью вызова процедуры slave свою локальную часть массива а, после чего посылают ее главному процессу. Процесс 0 сначала инициализирует неблокирующие приемы от всех остальных процессов, после чего дожидается прихода хотя бы одного сообщения. Для пришедших сообщений процесс 0 вызывает процедуру обработки master, после чего снова выставляет неблокирующие приемы. Таким образом, процесс 0 обрабатывает те порции данных, которые готовы на данный момент. При этом для корректности работы программы нужно обеспечить, чтобы процесс 0 успевал обработать приходящие сообщения, то есть, чтобы процедура slave работала значительно дольше процедуры master (в противном случае и распараллеливание не имеет особого смысла). Кроме того, в примере написан бесконечный цикл, поэтому для конкретной программы нужно предусмотреть условие завершения.

program example10
include 'mpif.h'
integer rank, size, ierr, N, MAXPROC
parameter(N = 1000, MAXPROC = 12 8)
integer req(MAXPROC), num, indexes(MAXPROC)
integer statuses(MPI_STATUS_SIZE, MAXPROC)
double precision a(N, MAXPROC)
call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr) call MPI_COMM_RANK
 (MPI_COMM_WORLD, rank, ierr) if(rank .ne. 0) then do while(.TRUE.) call slave(a, N)
call MPI_SEND(a, N, MPI_DOUBLE_PRECISION, 0, 5,& MPI_COMM_WORLD, ierr)
end do else
do i = 1, size-1
call MPI_IRECV(a(1, i), N, MPI_DOUBLE_PRECISION, i,& 5, MPI_COMM_WORLD, req(i), ierr)
end do do while(.TRUE.)
call MPI_WAITSOME(size-1, req, num, indexes, & statuses, ierr)
do i = 1, num
call master(a(1, indexes(i)), N) call MPI_IRECV(a(1, indexes(i)), N, & MPIDOUBLEPRECISION,
& indexes(i), 5, MPI_COMM_WORLD,
& req(indexes(i)), ierr)
end do end do end if
call MPI_FINALIZE(ierr) end
subroutine slave(a, n) double precision a integer n
С обработка локальной части массива а 
end
subroutine master(a, n) double precision a integer n
С обработка массива а 
End
MPI_TEST(REQUEST, FLAG, STATUS, IERR)
LOGICAL FLAG
INTEGER REQUEST, IERR, STATUS(MPI_STATUS_SIZE)
Листинг 3.2.

Проверка завершенности асинхронной операции MPI_ISEND ИЛИ MPI_IRECV, ассоциированной с идентификатором REQUEST. В параметре FLAG возвращается значение .TRUE., если операция завершена, и значение .FALSE. - в противном случае (в языке Си - 1 или о соответственно). Если завершена процедура приема, то атрибуты и длину полученного сообщения можно определить обычным образом с помощью параметра STATUS. После выполнения процедуры соответствующий элемент параметра REQUEST устанавливается в значение MPI_REQUEST_NULL.

MPI_TESTALL(COUNT, REQUESTS, FLAG, STATUSES, IERR)
LOGICAL FLAG
INTEGER COUNT, REQUESTS(*), STATUSES(MPI_STATUS_SIZE,*), IERR

Проверка завершенности COUNT асинхронных операций, ассоциированных с идентификаторами REQUESTS. В параметре FLAG процедура возвращает значение .TRUE, (в языке Си - l), если все операции, ассоциированные с указанными идентификаторами, завершены. В этом случае параметры сообщений будут указаны в массиве STATUSES. ЕСЛИ какая-либо из операций не завершилась, то возвращается .FALSE, (В языке Си - о), и определенность элементов массива STATUSES не гарантируется. После выполнения процедуры соответствующие элементы параметра REQUESTS устанавливаются в значение MPI_REQUEST_NULL.

MPI_TESTANY(COUNT, REQUESTS, INDEX, FLAG, STATUS, IERR)
LOGICAL FLAG
INTEGER COUNT, REQUESTS(*), INDEX, STATUS(MPI_STATUS_SIZE), IERR

Проверка завершенности хотя бы одной асинхронной операции, ассоциированной с идентификатором из массива REQUESTS.В параметре FLAG возвращается значение .TRUE, (В языке Си - 1), если хотя бы одна из операций асинхронного обмена завершена, при этом INDEX содержит номер соответствующего элемента в массиве REQUESTS, a STATUS - параметры сообщения. В противном случае в параметре FLAG будет возвращено значение .FALSE, (В языке Си - 0). Если к моменту вызова завершились несколько из ожидаемых операций, то случайным образом будет выбрана одна из них. После выполнения процедуры соответствующий элемент параметра REQUESTS устанавливается в значение MPI_REQUEST_NULL.

MPI_TESTSOME(INCOUNT, REQUESTS, OUTCOUNT, INDEXES, STATUSES, IERR)
INTEGER INCOUNT, REQUESTS(*), OUTCOUNT, INDEXES(*), IERR,STATUSES(MPI_STATUS_SIZE,*)

Аналог процедуры MPI_WAITSOME, НО возврат происходит немедленно. Если ни одна из тестируемых операций к моменту вызова не завершилась, то значение OUTCOUNT будет равно нулю.

Следующий пример демонстрирует применение неблокирующих операций для реализации транспонирования квадратной матрицы, распределенной между процессами по строкам. Сначала каждый процесс локально определяет nl строк массива, а затем при помощи неблокирующих операций MPI_ISEND и MPI_IRECV инициализируются все необходимые для транспонирования обмены данными. На фоне начинающихся обменов каждый процесс транспонирует свою локальную часть массива а. После этого процесс при помощи вызова процедуры MPI_WAITANY дожидается прихода сообщения от любого другого процесса и транспонирует полученную от данного процесса часть массива а. Обработка продолжается до тех пор, пока не будут получены сообщения от всех процессов. В конце исходный массив а и транспонированный массив Ь распечатываются.

program example11
include 'mpif.h'
integer ierr, rank, size, N, nl, i, j
parameter (N = 9)
double precision a(N, N), b(N, N)
call MPI_INIT(ierr)
call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierr)
call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
nl = (N-1)/size+1
call work (a, b, N, nl, size, rank)
call MPI_FINALIZE(ierr)
end
subroutine work(a, b, n, nl, size, rank) include 'mpif.h'
integer ierr, rank, size, n, MAXPROC, nl, i, j, ii, jj, ir parameter (MAXPROC = 64)
 double precision a(nl, n), b(nl, n), c integer irr, 
  status(MPI_STATUS_SIZE), req(MAXPROC*2) do i = 1, nl do j = 1, n
ii = i+rank*nl
if(ii .le. n) a(i, j) = 100*ii+j end do end do
do ir = 0, size-1
if(ir .ne. rank) &      call MPI_IRECV(b(1, ir*nl+1), nl*nl,
& MPI_DOUBLE_PRECISION, ir,
& MPI_ANY_TAG, MPI_COMM_WORLD,
& req(ir+l), ierr) end do
req(rank+1) = MPI_REQUEST_NULL
do ir = 0, size-1
if(ir .ne. rank)
&     call MPI_ISEND(a(1, ir*nl+1), nl*nl,
& MPI_DOUBLE_PRECISION, ir,
& 1, MPI_COMM_WORLD,
& req(ir+1+size), ierr)
end do
ir = rank do i = 1, nl ii = i+ir*nl do j = i+1, 
 nl jj = j+ir*nl b(i, j j) = a(j, ii) b(j, ii) = a(i, j j) end do
b(i, ii) = a(i, ii) end do
do irr = 1, size-1
call MPI_WAITANY(size, req, ir, status, ierr) ir = ir-1 
 do i = 1, nl ii = i+ir*nl do j = i+1, nl jj = j+ir*nl
 с = b(i, jj) b(i, j j) = b(j, ii) b(j, ii) = c end do end do end do
do i = 1, nl do j = 1, N
ii = i+rank*nl
if(ii .le. n) print *, 'process ', rank,
& ': a(', ii, ', ', j, ') =', a(i,j),
& ', b(', ii, ', ', j, ') =', b(i,j)
end do
 end do 
end
Листинг 3.3.

Отложенные запросы на взаимодействие

Процедуры данной группы позволяют снизить накладные расходы, возникающие в рамках одного процессора при обработке приема/передачи и перемещении необходимой информации между процессом и сетевым контроллером. Часто в программе приходится многократно выполнять обмены с одинаковыми параметрами (например, в цикле). В этом случае можно один раз инициализировать операцию обмена и потом многократно ее запускать, не тратя на каждой итерации дополнительного времени на инициализацию и заведение соответствующих внутренних структур данных. Кроме того, таким образом, несколько запросов на прием и/или передачу могут объединяться вместе для того, чтобы далее их можно было бы запустить одной командой (впрочем, это совсем необязательно хорошо, поскольку может привести к перегрузке коммуникационной сети).

Способ приема сообщения никак не зависит от способа его посылки: сообщение, отправленное с помощью отложенных запросов либо обычным способом, может быть принято как обычным способом, так и с помощью отложенных запросов.

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

Формирование отложенного запроса на посылку сообщения. Сама операция пересылки при этом не начинается!

Аналогично трем модификациям процедур MPI_SEND И MPI_ISEND, предусмотрены три дополнительных варианта процедуры MPI_SEND_INIT:

  • MPI_BSEND_INIT - формирование отложенного запроса на передачу сообщения с буферизацией;
  • MPI_SSEND_INIT - формирование отложенного запроса на передачу сообщения с синхронизацией;
  • MPI_RSEND_INIT - формирование отложенного запроса на передачу сообщения по готовности.
MPI_RECV_INIT(BUF, COUNT, DATATYPE, SOURCE, MSGTAG, COMM,
REQUEST, IERR)
<type> BUF(*)
INTEGER COUNT, DATATYPE, SOURCE, MSGTAG, COMM, REQUEST, IERR

Формирование отложенного запроса на прием сообщения. Сама операция приема при этом не начинается!

MPI_START(REQUEST, IERR) INTEGER REQUEST, IERR

Инициализация отложенного запроса на выполнение операции обмена, соответствующей значению параметра REQUEST. Операция запускается как неблокирующая.

MPI_STARTALL(COUNT, REQUESTS, IERR) INTEGER COUNT, REQUESTS, IERR

Инициализация COUNT отложенных запросов на выполнение операций обмена, соответствующих значениям первых COUNT элементов массива REQUESTS. Операции запускаются как неблокирующие.

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

MPI_REQUEST_FREE(REQUEST, IERR) INTEGER REQUEST, IERR

Данная процедура удаляет структуры данных, связанные с параметром REQUEST. После ее выполнения параметр REQUEST устанавливается в значение MPI_REQUEST_NULL. Если операция, связанная с этим запросом, уже выполняется, то она будет завершена.

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

prev = rank - 1
next = rank + 1
if(rank .eq. 0) prev = size - 1
if(rank .eq. size - 1) next = 0
call MPI_RECV_INIT(rbuf(1), 1, MPI_REAL, prev, 
 5, & MPI_COMM_WORLD, reqs(1), ierr)
call MPI_RECV_INIT(rbuf(2), 1, MPI_REAL, next, 
 6, & MPI_COMM_WORLD, reqs(2), ierr)
call MPI_SEND_INIT(sbuf(1), 1, MPI_REAL, prev, 
 6, & MPI_COMM_WORLD, reqs(3), ierr)
call MPI_SEND_INIT(sbuf(2), 1, MPI_REAL, next, 
 5, & MPI_COMM_WORLD, reqs(4), ierr)
do i = . . .
sbuf(1) = . . .
sbuf(2)
call MPI_STARTALL(4, reqs, ierr)
call MPI_WAITALL(4, reqs, stats, ierr);
end do
call MPI_REQUEST_FREE(reqs(1) , ierr)
call MPI_REQUEST_FREE(reqs(2), ierr)
call MPI_REQUEST_FREE(reqs(3) , ierr)
call MPI_REQUEST_FREE(reqs(4), ierr)
< Лекция 2 || Лекция 3: 123 || Лекция 4 >
Артем Калашник
Артем Калашник
Украина
Анастасия Ёлкина
Анастасия Ёлкина
Украина