Introduction
In our last lesson, we explored blocking point-to-point communication with MPI_Send and MPI_Recv. You learned how this model works and, crucially, how its blocking nature can lead to deadlocks if not carefully managed. While safe, this synchronous approach can be inefficient, forcing a process to sit idle while waiting for a message.
Today, we will address that inefficiency. This lesson introduces non-blocking communication, a powerful technique for improving the performance of your parallel programs. Our goal is to use the non-blocking functions MPI_Isend and MPI_Irecv, along with MPI_Wait, to overlap communication with computation. This allows a process to continue doing useful work while its messages are being transferred in the background, effectively hiding communication latency and making your application more scalable.
1. From Blocking to Non-Blocking Communication
The fundamental difference between blocking and non-blocking communication is when the function call returns.
- Blocking calls (
MPI_Send,MPI_Recv) do not return until the communication is complete (or at least, until the application's memory buffer is safe to be reused). - Non-blocking calls (
MPI_Isend,MPI_Irecv) return immediately, initiating the communication and handing it off to the MPI library to manage in the background.
This "fire and forget" approach is the key to unlocking performance, but it requires a new way to track the operation's status.

Let's examine the new functions and the mechanisms for managing them.
Non-blocking Communication - OxRSE Training
The Oxford Research Software Engineering group provides an excellent guide on this topic. First, let's understand the motivation and the function signatures.
Please read the sections 'Why bother with non-blocking communication?', 'So, how do I use non-blocking communication?', and the first part of 'Point-to-point communication'. Focus on: The concept of hiding communication latency by overlapping it with computation. The 'I' prefix for 'Immediate' functions. The role of the MPI_Request handle in tracking the operation. The purpose of MPI_Wait and why it is essential for correctness.
As you've just read, every non-blocking operation requires two steps:
- Initiation: Call
MPI_IsendorMPI_Irecv. These functions look very similar to their blocking counterparts, but with one crucial addition: a pointer to anMPI_Requestobject. MPI populates this object to serve as a handle for the pending operation. - Completion Test: At a later point, call
MPI_Wait. This function takes theMPI_Requesthandle and blocks until the corresponding operation has truly finished.
The C++ prototypes are:
// Initiates a non-blocking send
int MPI_Isend(
void* data,
int count,
MPI_Datatype datatype,
int destination,
int tag,
MPI_Comm communicator,
MPI_Request* request // The new handle
);
// Initiates a non-blocking receive
int MPI_Irecv(
void* data,
int count,
MPI_Datatype datatype,
int source,
int tag,
MPI_Comm communicator,
MPI_Request* request // The new handle
);
// Waits for a non-blocking operation to complete
int MPI_Wait(
MPI_Request* request,
MPI_Status* status
);
The Golden Rule of Non-Blocking Communication
Once you call MPI_Isend, you have promised MPI that you will not modify the send buffer until the operation is complete. Likewise, after calling MPI_Irecv, you must not read from the receive buffer until the operation is complete. The only way to know for sure that an operation is complete is to get a successful return from MPI_Wait.
Violating this rule leads to a race condition and undefined behavior. The data sent could be corrupted, or the data received could be incomplete.
2. Overlapping Communication with Computation
The time gap between initiating the communication (MPI_Isend/MPI_Irecv) and waiting for its completion (MPI_Wait) is where the magic happens. If you can fill this gap with useful computation that doesn't depend on the communication buffers, you have successfully hidden the communication latency.
The following flowchart visualizes this concept perfectly. The ideal case (a) is what we aim for.

Let's look at the code structure that achieves this overlap, adapted from the OxRSE guide.
MPI_Request request;
MPI_Status status;
int important_data[16];
int my_rank;
// Assume my_rank and important_data are set
if (my_rank == 0) {
// 1. Initiate the send. This returns immediately.
MPI_Isend(important_data, 16, MPI_INT, 1, 0, MPI_COMM_WORLD, &request);
} else if (my_rank == 1) {
// 1. Initiate the receive. This also returns immediately.
MPI_Irecv(important_data, 16, MPI_INT, 0, 0, MPI_COMM_WORLD, &request);
}
// 2. Do other useful work that does NOT touch 'important_data'.
// This computation happens IN PARALLEL with the data transfer.
do_some_independent_calculation();
// 3. Wait for the communication to complete.
// If the independent calculation took long enough, this might not block at all!
MPI_Wait(&request, &status);
// 4. Now it's safe to use the data.
// Rank 0 can modify 'important_data' again.
// Rank 1 can read the received values from 'important_data'.
do_something_with_the_data(important_data);
This pattern is the cornerstone of high-performance MPI programming.
3. Non-Blocking Calls and Deadlock Prevention
As a welcome side effect, non-blocking communication can elegantly solve the deadlock problem we discussed in the previous lesson. Recall the deadlock scenario:
- Process 0:
MPI_Send(to=1), thenMPI_Recv(from=1) - Process 1:
MPI_Send(to=0), thenMPI_Recv(from=0)
Both processes get stuck on MPI_Send because neither has posted a matching MPI_Recv.
Now, consider the non-blocking equivalent:
- Process 0:
MPI_Isend(to=1),MPI_Irecv(from=1), thenMPI_Waitfor both. - Process 1:
MPI_Isend(to=0),MPI_Irecv(from=0), thenMPI_Waitfor both.
Since MPI_Isend and MPI_Irecv return immediately, both processes are able to post both their send and receive requests without blocking. The MPI library can then resolve the pending communications. No deadlock!
Non-blocking Communication - OxRSE Training
Let's revisit the OxRSE resource to see a concrete example of how non-blocking calls prevent deadlock.
Please read the section 'What About Deadlocks?'. Compare the deadlocking blocking code with the non-blocking solution. Notice the use of MPI_Waitall in the solution, which is a convenient function to wait on multiple requests at once.
4. Alternative Completion: MPI_Test
Sometimes, you might be in a loop where you can do a small piece of work and then check if your communication is done. For this, MPI_Wait is too heavy, as it will block if the communication isn't finished.
The alternative is MPI_Test. It is the non-blocking version of MPI_Wait:
- It checks the status of a request.
- It returns immediately.
- It sets a flag to
trueif the operation is complete, andfalseotherwise.
int MPI_Test(
MPI_Request* request,
int* flag, // Output: will be true if complete
MPI_Status* status
);
You can use this in a loop to continue doing work until the communication is finished.
Non-blocking Communication - OxRSE Training
The OxRSE guide also provides a good explanation and example for MPI_Test.
Read the section 'To wait, or not to wait' to understand how MPI_Test works and see a typical usage pattern within a while loop.
5. Hands-on Practice: A Non-Blocking Ring
It's time to apply what you've learned. In the last lesson, you implemented a ring program using blocking sends and receives. Now, your task is to rewrite it using a more efficient, fully non-blocking pattern.
Your Task:
Modify your ring program to use non-blocking communication. For each process i (including rank 0):
- Initiate a non-blocking receive (
MPI_Irecv) to get a token from processi-1(with wraparound). - Initiate a non-blocking send (
MPI_Isend) to send its token to processi+1(with wraparound). - Wait for both the send and receive to complete using
MPI_Waitall, which is likeMPI_Waitbut for an array of requests. - After the wait completes, print the message confirming the token was received.
This pattern—posting all receives, then all sends, then waiting for all completions—is a very common and robust way to perform neighbor exchanges in parallel applications like simulations on a grid.
Hint: You will need two MPI_Request objects, one for the send and one for the receive. MPI_Waitall takes an array of these requests.
// Example of using MPI_Waitall
MPI_Request requests[2];
MPI_Status statuses[2];
// ... post Isend and Irecv, storing handles in requests[0] and requests[1]
// Wait for BOTH operations to complete
MPI_Waitall(2, requests, statuses);
Take about 15 minutes to implement this. The following resource contains a complete solution to a very similar problem if you get stuck.
Non-blocking Communication - OxRSE Training
This exercise is a practical challenge. The final section of the OxRSE guide provides a skeleton and a full solution for a similar problem that should give you all the clues you need.
Review the exercise under 'Try it yourself'. Pay close attention to the solution code. It demonstrates how to set up the MPI_Irecv and MPI_Isend and then wait on them. Your implementation will be very similar.
Conclusion
You have now added a critical tool to your parallel programming toolkit. By moving from blocking to non-blocking communication, you can write more sophisticated and performant MPI applications that keep the CPU busy instead of waiting on the network.
Key Takeaways:
- Non-blocking Initiation:
MPI_IsendandMPI_Irecvstart a communication and return immediately, allowing your program to continue. - Tracking and Completion: The
MPI_Requesthandle is used to track a pending operation, andMPI_Wait(orMPI_Waitall) is used to ensure it has completed before using the buffers. - Overlapping for Performance: The primary goal is to perform independent computations between the initiation and completion calls, hiding communication latency.
- Deadlock Avoidance: Non-blocking calls provide a natural way to avoid many common deadlock scenarios.
Preview of the Next Lesson
So far, we have only dealt with point-to-point communication between two processes. However, many parallel algorithms require patterns involving groups of processes, such as broadcasting data from one process to all others, or gathering results from all processes to one. In the next lesson, we will explore collective communication with functions like MPI_Bcast and MPI_Reduce that efficiently implement these group operations.