acquireReleaseConsume

memory_order_consume

std::memory_order_consume is the most legendary of the six memory models. That’s for two reasons. On the one hand, std::memory_order_consume is extremely hard to get. On the other hand – that may change in the future – no compiler supports it.

 

How can it happen that a compiler supports the C++11 standard but doesn’t support the memory model std::memory_order_consume? The answer is that compiler maps std::memory_order_consume to std::memory_order_acquire. That is fine because both are load or acquire operations. std::memory_order_consume requires weaker synchronization and ordering constraints. So the release-acquire ordering is potentially slower than the release-consume ordering, but the crucial point is well-defined. 

To understand the release-consume ordering, it’s a good idea to compare it with the release-acquire ordering. I speak in the post explicitly from the release-acquire ordering and not from the acquire-release semantic to emphasize the strong relationship of std::memory_order_consume and std::memory_order_acquire.

Release-acquire ordering

As a starting point, I use a program with two threads t1 and t2. t1 plays the role of the producer, t2 the role of the consumer. The atomic variable ptr helps to synchronize the producer and consumer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// acquireRelease.cpp

#include <atomic>
#include <thread>
#include <iostream>
#include <string>
 
std::atomic<std::string*> ptr;
int data;
std::atomic<int> atoData;
 
void producer(){
    std::string* p  = new std::string("C++11");
    data = 2011;
    atoData.store(2014,std::memory_order_relaxed);
    ptr.store(p, std::memory_order_release);
}
 
void consumer(){
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)));
    std::cout << "*p2: " << *p2 << std::endl;
    std::cout << "data: " << data << std::endl;
    std::cout << "atoData: " << atoData.load(std::memory_order_relaxed) << std::endl;
}
 
int main(){
    
    std::cout << std::endl;
    
    std::thread t1(producer);
    std::thread t2(consumer);
    
    t1.join();
    t2.join();
    
    std::cout << std::endl;
    
}

 

Before I analyze the program, I want to introduce a small variation. I replace in line 21 the memory model std::memory_order_acquire with std::memory_order_consume.

Rainer D 6 P2 500x500

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    Release-consume ordering

     

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    // acquireConsume.cpp
    
    #include <atomic>
    #include <thread>
    #include <iostream>
    #include <string>
     
    std::atomic<std::string*> ptr;
    int data;
    std::atomic<int> atoData;
     
    void producer(){
        std::string* p  = new std::string("C++11");
        data = 2011;
        atoData.store(2014,std::memory_order_relaxed);
        ptr.store(p, std::memory_order_release);
    }
     
    void consumer(){
        std::string* p2;
        while (!(p2 = ptr.load(std::memory_order_consume)));
        std::cout << "*p2: " << *p2 << std::endl;
        std::cout << "data: " << data << std::endl;
        std::cout << "atoData: " << atoData.load(std::memory_order_relaxed) << std::endl;
    }
     
    int main(){
        
        std::cout << std::endl;
        
        std::thread t1(producer);
        std::thread t2(consumer);
        
        t1.join();
        t2.join();
        
        std::cout << std::endl;
        
    }
    

     

    That was easy. But now, the program has undefined behavior. That statement is very hypothetical because my compiler implements std::memory_order_consume by std::memory_order_acquire. So under the hood, both program does the same.

    Release-acquire versus Release-consume ordering

    The output of the programs is identical.

     acquireReleaseConsume

    Although I repeat myself, I want to sketch in a few words why the first program acquireRelease.cpp is well defined.

    The store operation in line 16 synchronizes-with the load operation in line 21. The reason is, that the store operation uses std::memory_order_release, and the load operation uses std::memory_order_acquire. That was the synchronization. What about the ordering constraints of the release-acquire ordering? The release-acquire ordering guarantees that all operations before the store operation (line 16) are available after the load operation (line 21). So the release-acquire operation orders, in addition to the access on the non-atomic variable data (line 14) and the atomic variable atoData (line 15). That holds, although atoData uses the std::memory_order_relaxed memory model.

    The key question is. What happens if I replace the program std::memory_order_acquire with std::memory_order_consume?

    Data dependencies with std::memory_order_consume

    The std::memory_order_consume is about data dependencies on atomics. Data dependencies exist in two ways, at first carries-a-dependency-to in a thread and dependency-ordered_before between two threads. Both dependencies introduce a happens-before relation. That is the kind of relation a well-defined program needs.  But what means carries-a-dependency-to and dependency-order-before?

    • carries-a-dependency-to: If the result of an operation A is used as an operand of an operation B, then: A carries-a-dependency-to B.
    • dependency-ordered-before: A store operation (with std::memory_order_release, std::memory_order_acq_rel or std::memory_order_seq_cst), is dependency-ordered-before a load operation B (with std::memory_order_consume) if the result of the load operation B is used in a further operation C in the same thread. Operations B and C have to be in the same thread.

    Of course, I know from experience that both definitions are difficult to digest. So I will use a graphic to explain them visually.

    dependency

    The expression ptr.store(p, std::memory_order_release) is dependency-ordered-before while (!(p2 = ptr.load(std::memory_order_consume))), because in the following line std::cout << “*p2: ” << *p2 << std::endl the result of the load operation will be read. Further, holds: while (!(p2 = ptr.load(std::memory_order_consume)) carries-a-dependency-to std::cout << “*p2: ” << *p2 << std::endl, because the output of *p2 uses the result of the ptr.load operation.

    But we have no guarantee for the following outputs of data and atoData. That’s because both have no carries-a-dependency relation to the ptr.load operation. But it gets even worse. Because data is a non-atomic variable, there is a race condition on data. The reason is that both threads can access data simultaneously, and thread t1 wants to modify data. Therefore, the program is undefined. 

    What’s next?

    I admit that was a challenging post. In the next post, I deal with the typical misunderstanding of the acquire-release semantic. That happens if the acquire operation is performed before the release operation.

     

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *