Published: March 6, 2025 | Reading Time: 4 minutes
Message passing is a fundamental concept in C++ and a key aspect of object-oriented programming. It involves objects communicating through the exchange of messages, typically via method or function calls. This mechanism encourages collaboration among objects and supports a modular and flexible software design. In this comprehensive guide, we'll explore the essentials of message passing in C++, examine its advantages, and discuss how it can enhance the efficiency of software development.
A message is a block of data or a well-structured request by one object (the sender) to another (the receiver) for invoking a specific function or executing a particular task. Messages can be transmitted through different channels, such as local networks, remote networks, shared memory, or inter-process communication. They serve to signal events, requests, or responses between components, as well as for synchronization and coordination.
The critical components of a message include:
Message passing is the process of communicating information between objects through a specific logical entity called a message. This mechanism is crucial for building reliable and modular software systems. It is also a central notion in object-oriented programming and encapsulation, which involves interfacing objects through well-defined public methods or functions instead of their internal details.
Here is the implementation of message passing in C++ with an example program:
#include <iostream>
#include <string>
class Car {
public:
void accelerate() {
std::cout << "The car is accelerating." << std::endl;
}
void stop() {
std::cout << "The car has stopped." << std::endl;
}
};
class Driver {
public:
void sendMessage(Car& car, const std::string& command) {
if (command == "accelerate") {
car.accelerate(); // Sending a message to accelerate
} else if (command == "stop") {
car.stop(); // Sending a message to stop
} else {
std::cout << "Unknown command." << std::endl;
}
}
};
int main() {
Car myCar;
Driver driver;
driver.sendMessage(myCar, "accelerate"); // Sending a message to the car
driver.sendMessage(myCar, "stop"); // Sending another message to stop
return 0;
}
Car class has methods accelerate() and stop(), while the Driver class has a method sendMessage() that sends commands to the CarDriver sends commands ("accelerate" or "stop") to the Car, demonstrating a simple message-passing mechanismmain() function, messages are sent to the car, resulting in the outputThe car is accelerating.
The car has stopped.
Message passing differs from the previous two methods. It allows objects to share data by exchanging messages. These steps involve:
The message passing can be two types:
| Aspect | Synchronous Message Passing | Asynchronous Message Passing |
|---|---|---|
| Waiting Behavior | The sender waits for the receiver to get the message | The sender does not wait for the receiver's acknowledgement before execution continues |
| Blocking | Blocks the sender waits until the receiver acknowledges receipt | Non-blocking; the sender continues immediately |
| Synchronization | Provides for synchronisation of the involved parties during communication | Enables independent operation of sender and receiver |
| Implementation | It is simpler to implement, and flow control is relatively easy | More complex; needs mechanisms for handling delivery and ordering |
| Use Cases | Suitable for applications needing immediate feedback and confirmation | It is ideal for applications where responsiveness and parallelism are prioritised |
| Performance | The sender will always create delays if it waits and the receiver is busy, thus affecting performance | Improves throughput by allowing multiple operations to proceed simultaneously |
The code implementation of message passing in C++ through Boost Libraries, i.e., Boost.Interprocess for inter-thread and Boost.Asio for network communication (server-client approach):
#include <boost/interprocess/ipc/message_queue.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <thread>
#include <chrono>
using namespace boost::interprocess;
using namespace boost::asio;
using ip::tcp;
void sender() {
message_queue mq(open_or_create, "mq", 100, sizeof(int));
for (int i = 0; i < 10; ++i) {
mq.send(&i, sizeof(i), 0);
std::cout << "Sent: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void receiver() {
message_queue mq(open_or_create, "mq", 100, sizeof(int));
for (int i = 0; i < 10; ++i) {
int msg;
size_t size;
unsigned int prio;
mq.receive(&msg, sizeof(msg), size, prio);
std::cout << "Received: " << msg << std::endl;
}
}
void server() {
io_service ios;
tcp::acceptor acc(ios, tcp::endpoint(tcp::v4(), 12345));
std::cout << "Waiting for connection..." << std::endl;
tcp::socket sock(ios);
acc.accept(sock);
std::cout << "Client connected!" << std::endl;
char buf[512];
size_t len = sock.read_some(boost::asio::buffer(buf));
std::cout << "Received: " << std::string(buf, len) << std::endl;
}
void client() {
io_service ios;
tcp::resolver res(ios);
tcp::resolver::query qry("127.0.0.1", "12345");
tcp::resolver::iterator it = res.resolve(qry);
tcp::socket sock(ios);
connect(sock, it);
std::string msg = "Hello!";
sock.send(boost::asio::buffer(msg));
std::cout << "Sent: " << msg << std::endl;
}
int main() {
std::thread t1(sender);
std::thread t2(receiver);
t1.join();
t2.join();
message_queue::remove("mq");
std::thread s(server);
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Wait for server to start
std::thread c(client);
c.join();
s.join();
return 0;
}
Here is the explanation of boost libraries code:
sender() and receiver() functions utilize a message queue to send and receive messages between two threads of the same processsender() sends 10 integers (0 to 9) into the message queue, and the receiver() function reads them one by one and prints themmessage_queue::remove("mq") is used to delete the message queue after completing the communicationserver() procedure waits for a connection from a client on port 12345. After a connection from the client, the server reads the message from the client and prints itclient() procedure connects to the server, sends the string message ("Hello!"), and prints what it sends//Sender thread will print
Sent: 0
Sent: 1
Sent: 2
Sent: 3
Sent: 4
Sent: 5
Sent: 6
Sent: 7
Sent: 8
Sent: 9
//Receiver thread will print
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Received: 6
Received: 7
Received: 8
Received: 9
//Server prints
Waiting for connection...
Client connected!
Received: Hello!
//Client prints
Sent: Hello!
Here is an inter-thread message passing example in C++:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
bool ready = false;
void producer() {
for (int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate work
std::lock_guard<std::mutex> lock(mtx);
dataQueue.push(i);
ready = true; // Signal that data is ready
cv.notify_one(); // Notify consumer
std::cout << "Produced: " << i << std::endl;
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // Wait until data is ready
while (!dataQueue.empty()) {
int data = dataQueue.front();
dataQueue.pop();
std::cout << "Consumed: " << data << std::endl;
}
ready = false; // Reset the flag after consuming all data
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
The above C++ program implements a producer-consumer model using threads, mutexes, and condition variables. The producer() function generates integers from 0 to 9 and puts them in a shared queue, telling the consumer() when data can be processed. The consumer() waits until the producer signals, then takes the data out of the queue for processing. Locking mechanisms ensure that one thread locks its sections of access to the shared queue while waiting for the other threads to unlock theirs. Condition variables ensure the synchronisation of the interaction between the producer and consumer threads.
Produced: 0
Produced: 1
Produced: 2
Produced: 3
Produced: 4
Produced: 5
Produced: 6
Produced: 7
Produced: 8
Produced: 9
Consumed: 0
Consumed: 1
Consumed: 2
Consumed: 3
Consumed: 4
Consumed: 5
Consumed: 6
Consumed: 7
Consumed: 8
Consumed: 9
A message queue is a data structure that enables communication among performing independent units: either threads or processes. It lets units send or receive messages. They act as a holding area or buffer for messages until they are processed and provide asynchronous communication. Message queues can be based, for instance, on a queue made with standard library containers like std::queue, or they can be implemented from scratch when a specific set of requirements must be addressed.
Here are the key differences between method call and message passing:
| Aspect | Method Call | Message Passing |
|---|---|---|
| Definition | A method call means a direct call of a member function of an object | Message passing is a broader concept that allows inter-communication between objects through messages |
| Scope | Method calls represent a specific way to invoke behaviour on an object | Message Passing considers various communication mechanisms, including events and signals |
| Communication Type | It involves direct communication between two objects | It can involve multiple forms of communication, allowing for more flexible interactions |
| Coupling | The sender must explicitly know the receiver and call its method directly | The sender raises an event or sends a message that any subscribed receiver may handle |
| Flexibility | The receiver has a specific function defined to handle the call | Different receivers can respond differently to the same message, providing greater flexibility |
| Relationship | A method call is a particular instance of message passing focused on function invocation | Message passing is a general term comprising any mechanism for object communication |
| Parameters | Parameters are passed directly with the method call to provide necessary data | Parameters can be included in the message, allowing the receiver to handle them differently |
Messaging design patterns provide solutions to common communication challenges in distributed systems. These patterns facilitate the message exchange between components, so they are in high demand because they provide loose coupling and scalability in software development. Here are the message design patterns:
The publish-subscribe pattern allows multiple subscribers to receive messages from one point (a broker). This allows decoupling and high scaling levels because direct connections between subscribers and publishers are discarded.
The message queue pattern decouples producers from consumers, allowing for reliable and asynchronous message passing. Messages are queued until consumers are ready to process them, preventing data loss.
The request-reply pattern provides synchronous communication in which one component requests a response from another and waits for a response. Popular usage includes client-server architectures.
The message broker pattern is an intermediary that manages the exchange of messages between producers and consumers, providing routing, filtering, and transformation capabilities for more complex interactions.
The command pattern assigns requests to one of the objects; it allows queuing, parameterisation, and implementing undo functionality. This increases the flexibility of handling commands in applications.
Here are some best practices for message passing in C++:
In conclusion, message passing in C++ is an essential paradigm that facilitates communication between objects and processes, building greater modularity and flexibility in software design. By understanding its principles and implementing effective strategies, developers may develop robust applications that handle these complex interactions, remain performant, and provide decent reliability.
The message-passing technique involves sending a request from one object to another to perform an operation or return data.
Passing a message means invoking a method or function of another object by sending some kind of request, often carrying some data.
The three types of messages are:
Dynamic Binding refers to the methods in C++. The method call resolves at runtime, while polymorphism uses virtual functions. Message Passing tells the objects to communicate by invoking methods on each other. In dynamic binding, method resolution is based on a runtime; message passing includes sending requests for action and/or data between two objects.
Source: NxtWave - CCBP Blog
Original URL: https://www.ccbp.in/blog/articles/message-passing-in-cpp