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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

     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.

    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, Kris Kafka, 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, Dmitry Farberov, 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, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    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

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    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 *