Skip to main content
Parallel Programming · lesson 5 of 8
Parallel Programming intermediate Lesson 5 of 8

MPI Basics: a Real Deadlock That Only Shows Up at Scale

Two ranks calling MPI_Send before MPI_Recv works fine for a single int — and hangs forever for a half-million-element array. The same bug, passing at small scale and deadlocking in production.

By SkillByExample · Published

parallel-programming mpi deadlock point-to-point

MPI processes don’t share memory — the only way data moves between them is a message, sent and received explicitly. That explicitness is also where a very real deadlock comes from.

rank and size: every process knows its place

// mpi_hello.c
#include <stdio.h>
#include <mpi.h>

int main(int argc, char** argv) {
    MPI_Init(&argc, &argv);
    int rank, size;
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    printf("Hello from rank %d of %d\n", rank, size);
    MPI_Finalize();
    return 0;
}
mpicc -o mpi_hello mpi_hello.c
mpirun -np 4 ./mpi_hello
Hello from rank 0 of 4
Hello from rank 1 of 4
Hello from rank 2 of 4
Hello from rank 3 of 4

mpirun -np 4 launches four independent OS processes, each running the same program — rank is each process’s own identity within that group, not a thread ID within one process.

A basic send and receive

// mpi_sendrecv.c (basic Send/Recv, not to be confused with the MPI_Sendrecv call later)
if (rank == 0) {
    int value = 42;
    MPI_Send(&value, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
    printf("rank 0 sent %d to rank 1\n", value);
} else if (rank == 1) {
    int received;
    MPI_Recv(&received, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    printf("rank 1 received %d from rank 0\n", received);
}
mpirun -np 2 ./mpi_sendrecv
rank 0 sent 42 to rank 1
rank 1 received 42 from rank 0

The bug: a deadlock that only appears at scale

// mpi_deadlock.c
int buf[500000];
for (int i = 0; i < 500000; i++) buf[i] = rank;
int recvbuf[500000];

int other = (rank == 0) ? 1 : 0;
printf("rank %d: sending to rank %d\n", rank, other);
MPI_Send(buf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD);
MPI_Recv(recvbuf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("rank %d: done\n", rank);
mpicc -o mpi_deadlock mpi_deadlock.c
mpirun -np 2 ./mpi_deadlock
rank 1: sending to rank 0
rank 0: sending to rank 1
[hangs forever]

Both ranks print “sending” — and then nothing. Each is blocked inside MPI_Send, waiting for the other rank to call MPI_Recv before its own send can complete. But neither rank reaches MPI_Recv — both are still stuck on the MPI_Send line above it. This is a real, classic deadlock: two ranks each waiting on the other to do something neither will ever get to do.

Why the exact same pattern worked earlier

// mpi_small.c -- identical Send-then-Recv structure, one int instead of 500,000
MPI_Send(&value, 1, MPI_INT, other, 0, MPI_COMM_WORLD);
MPI_Recv(&received, 1, MPI_INT, other, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("rank %d: done, received %d\n", rank, received);
mpirun -np 2 ./mpi_small
rank 1: done, received 0
rank 0: done, received 1

No deadlock — MPI_Send is allowed to buffer a small message internally and return immediately, without waiting for the matching MPI_Recv at all. A single int fits in that buffer; 500,000 ints don’t. This is what makes the bug genuinely dangerous: the same code structure works perfectly with small test data and deadlocks the first time it runs against a realistically large one.

The fix: MPI_Sendrecv

// mpi_fixed.c
int other = (rank == 0) ? 1 : 0;
MPI_Sendrecv(buf, 500000, MPI_INT, other, 0,
             recvbuf, 500000, MPI_INT, other, 0,
             MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("rank %d: done, received first value %d from rank %d\n", rank, recvbuf[0], other);
mpicc -o mpi_fixed mpi_fixed.c
mpirun -np 2 ./mpi_fixed
rank 1: exchanging with rank 0
rank 0: exchanging with rank 1
rank 1: done, received first value 0 from rank 0
rank 0: done, received first value 1 from rank 1

MPI_Sendrecv combines both operations into one call that MPI implements to avoid exactly this ordering problem — both ranks can call it simultaneously, each naming who to send to and who to receive from, with no risk of both being stuck waiting on the other’s plain Send.

Practice

1. Fix the deadlock by reordering instead of using MPI_Sendrecv — one rank sends first, the other receives first.
int other = (rank == 0) ? 1 : 0;
if (rank == 0) {
    MPI_Send(buf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD);
    MPI_Recv(recvbuf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
} else {
    MPI_Recv(recvbuf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
    MPI_Send(buf, 500000, MPI_INT, other, 0, MPI_COMM_WORLD);
}
printf("rank %d: done\n", rank);
rank 0: done
rank 1: done

Breaking the symmetry works: rank 0’s Send has rank 1 already waiting in Recv to accept it, and rank 1’s later Send has rank 0 (past its own Recv by then) ready to receive. No buffering luck required, unlike the small-message case.

2. Check how many ranks were actually launched from inside the program.
int size;
MPI_Comm_size(MPI_COMM_WORLD, &size);
printf("running with %d ranks\n", size);
mpirun -np 6 ./mpi_hello
Hello from rank 5 of 6
Hello from rank 0 of 6
Hello from rank 1 of 6
Hello from rank 2 of 6
Hello from rank 4 of 6
Hello from rank 3 of 6

Six independent processes this time, printing in whatever order the OS scheduled them — rank 0 is not guaranteed to print first, the same non-determinism as OpenMP’s thread ordering, just across processes instead of threads.

3. Confirm the deadlock reproduces with more ranks all trying to exchange large messages pairwise.

With 4 ranks all attempting the same large-message Send-before-Recv pattern pairwise (0↔1, 2↔3), the deadlock reproduces independently in each pair — it’s a property of any two communicating ranks using this unsafe pattern, not something that only happens with exactly 2 ranks total. Fixing it requires applying MPI_Sendrecv (or the reordering from task 1) to every communicating pair, not just one.

Next: collective communication — broadcasting, reducing, and scattering data across every rank at once.

Frequently Asked Questions

What's the fundamental difference between MPI and OpenMP?
OpenMP threads share one process's memory directly — a variable one thread writes is immediately visible to another. MPI processes (ranks) have entirely separate memory; the only way one rank's data reaches another is by explicitly sending and receiving a message. This is the shared-memory-vs-distributed-memory distinction, and it's why MPI has Send/Recv at all where OpenMP just has shared variables.
Why did the Send/Send pattern deadlock with a large message but not a small one?
MPI_Send is allowed to buffer a small message internally and return immediately, without waiting for the matching MPI_Recv — which is exactly what let the one-int version complete fine. Once the message is too large for that internal buffer, MPI_Send blocks until the receiver actually starts receiving, and since both ranks are stuck in their own blocking Send waiting for the other to call Recv, neither ever does.
Why is this bug particularly dangerous compared to a bug that fails immediately?
The exact buffering threshold isn't guaranteed by the MPI standard and varies by implementation and message size — a program can pass testing with small messages, ship, and then deadlock in production the first time someone runs it on a realistically large dataset. Relying on MPI_Send's buffering behavior instead of writing genuinely deadlock-safe communication is a latent bug, not a working pattern.
How does MPI_Sendrecv avoid the deadlock?
It combines a send and a receive into one call that MPI implements internally to avoid exactly this ordering problem — both ranks can call it simultaneously, each specifying who to send to and who to receive from, and MPI handles the coordination so neither rank blocks waiting on the other's Send to finish first.
Are there other ways to fix a Send/Send deadlock besides MPI_Sendrecv?
Yes — reordering so one rank does Send-then-Recv while the other does Recv-then-Send breaks the symmetry that caused the deadlock, and non-blocking MPI_Isend/MPI_Irecv let both ranks post their sends and receives without either one blocking, then wait for completion afterward. MPI_Sendrecv is usually the simplest correct option when it fits the communication pattern.