In this article, we will discuss the std::memory_order enum in C++ with its example.
- The order in which memory accesses, including conventional, and non-atomic memory accesses, are to be placed around an atomic operation is specified by the std::memory_order function.
- When numerous threads simultaneously read and writeto multiple variables in a multi-core system without any limitations, one thread may notice that the values change in a different order than the order in which another thread wrote them.
- Different reader threads may even appear to have different modifications in the same order.
- Because of compiler changes permitted by the memory model, certain identical effects may even manifest on systems with a single CPU.
- Operations that are assured to be carried out without interference from other threads are known as atomic operations .
- It supports for atomic operations in C++ is given by the <atomic> header, which includes types like std::atomic and functions like std::atomicload and std::atomicstore.
- All atomic actions in the library have default behavior that allows for sequentially consistent ordering (see discussion below).
- The library's atomic actions can be given an extra std::memory_order parameter to indicate the precise constraints, beyond atomicity, that the compiler and processor must apply for that operation.
Atomic Functions:
Memory Order Enum in std:
The std::memory_order enum defines the memory ordering constraints for atomic operations. It presents various options, each representing a unique level of ordering and synchronization guarantee. The main selections include:
memoryorderrelaxed: This represents the minimum level of ordering constraints. It allows the compiler and processor to rearrange memory operations as long as the program's behavior aligns with the sequential consistency model. As long as the end result mirrors what would happen if the operations were executed in their initial sequence, reordering actions is permissible.
memoryorderconsume: Unlike memoryorderacquire, this synchronization type is not as efficient. It is specifically intended for scenarios requiring consumption, such as retrieving a value from memory that relies on another previously read value. Its purpose is to guarantee that the dependent operation occurs only after the consuming operation.
memoryorderacquire: This memory ordering constraint guarantees that any preceding operations relying on the data read from memory will not be reordered around the read-modify-write (RMW) operation executed by the atomic operation. It guarantees the retrieval of the latest value written by a different thread.
memoryorderrelease: This ensures that no further operations relying on the recorded value are executed after the atomic write operation. It guarantees visibility of the written value to other threads.
Combining memoryorderrelease and memoryorderacquire leads to memoryorderacq_rel: This guarantees that the latest value written by a different thread is fetched and that the written value is observable by other threads.
The memoryorderseq_cst provides the most reliable ordering assurance. It ensures that each atomic action's consistency is maintained concerning all other operations. This indicates that the execution sequence aligns with the program's order of appearance.
Pseudocode:
// Shared data
atomic int data
atomic bool ready
// Producer function
function producer():
// Produce some data
int value = 42
// Store the data with memory_order_release
data.store(value, memory_order_release)
// Signal readiness to the consumer
ready.store(true, memory_order_release)
// Consumer function
function consumer():
// Wait for the producer to signal readiness
while not ready.load(memory_order_acquire):
// Do nothing or yield
// Load the data with memory_order_acquire
int value = data.load(memory_order_acquire)
// Consume the data
print "The answer is " + value
// Main function
function main():
// Start the producer and consumer threads
start_thread(producer)
start_thread(consumer)
// Wait for the threads to finish
join_thread(producer)
join_thread(consumer)
Example:
Let's consider a scenario to demonstrate the std::memory_order enumeration in C++.
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> data;
std::atomic<bool> ready;
void producer() {
// Produce some data
int value = 42;
// Store the data with memory_order_release
data.store(value, std::memory_order_release);
// Signal readiness to the consumer
-ready.store(true, std::memory_order_release);
}
void consumer() {
// Wait for the producer to signal readiness
while (!ready.load(std::memory_order_acquire)) {
// Do nothing or yield
std::this_thread::yield();
}
// Load the data with memory_order_acquire
int value = data.load(std::memory_order_acquire);
// Consume the data
std::cout << "The answer is " << value << std::endl;
}
int main() {
// Start the producer and consumer threads
std::thread producer_thread(producer);
std::thread consumer_thread(consumer);
// Wait for the threads to finish
producer_thread.join();
consumer_thread.join();
return 0;
}
Output:
Conclusion:
In summary, you have the ability to specify the memory ordering prerequisites for atomic operations by utilizing the C++ std::memory_order enumeration. Understanding this memory ordering is crucial for developing precise and efficient concurrent applications. The specific demands of the program and the level of synchronization needed dictate the appropriate memory order selection.