TimelineCpp20CoreLanguage

Synchronization with Atomics in C++20

Sender/receiver workflows are pretty common for threads. In such a workflow, the receiver is waiting for the sender’s notification before it continues to work. There are various ways to implement these workflows. With C++11, you can use condition variables or promise/future pairs; with C++20, you can use atomics.

 TimelineCpp20CoreLanguage

There are various ways to synchronize threads. Each way has its pros and cons. Consequently,  I want to compare them. I assume you don’t know the details of condition variables or promises and futures. Therefore, I will give a short refresher.

Condition Variables

A condition variable can fulfill the role of a sender or a receiver. As a sender, it can notify one or more receivers.

 

// threadSynchronisationConditionVariable.cpp

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <vector>

std::mutex mutex_;
std::condition_variable condVar;

std::vector<int> myVec{};

void prepareWork() {                                        // (1)

    {
        std::lock_guard<std::mutex> lck(mutex_);
        myVec.insert(myVec.end(), {0, 1, 0, 3});            // (3)
    }
    std::cout << "Sender: Data prepared."  << std::endl;
    condVar.notify_one();
}

void completeWork() {                                       // (2)

    std::cout << "Worker: Waiting for data." << std::endl;
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck, [] { return not myVec.empty(); });
    myVec[2] = 2;                                           // (4)
    std::cout << "Waiter: Complete the work." << std::endl;
    for (auto i: myVec) std::cout << i << " ";
    std::cout << std::endl;
    
}

int main() {

    std::cout << std::endl;

    std::thread t1(prepareWork);
    std::thread t2(completeWork);

    t1.join();
    t2.join();

    std::cout << std::endl;
  
}

 

The program has two child threads:t1 and t2. They get their payload prepareWork and completeWork in lines (1) and (2). The function prepareWork notifies that it is done with the preparation of the work: condVar.notify_one(). While holding the lock, the thread t2 is waiting for its notification: condVar.wait(lck, []{ return not myVec.empty(); }). The waiting thread always performs the same steps. When it is waked up, it checks the predicate while holding the lock ([]{ return not myVec.empty();). If the predicate does not hold, it puts itself back to sleep. If the predicate holds,  it continues with its work. In the concrete workflow, the sending thread puts the initial values into the std::vector(3),  which the receiving thread completes (4).

threadSynchronisationConditionVariable

 

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.

     

    Condition variables have many inherent issues. For example, the receiver could be awakened without notification or could lose the notification. The first issue is spurious wakeup, and the second is lost wakeup. The predicate protects against both flaws. The notification would be lost when the sender sends its notification before the receiver is in the wait state and does not use a predicate.
    Consequently, the receiver waits for something that never happens. This is a deadlock. When you study the program’s output, you see that each second run would cause a deadlock if I would not use a predicate. Of course, it is possible to use condition variables without a predicate.

    If you want to know the sender/receiver workflow details and the traps of condition variables, read my previous post, “C++ Core Guidelines: Be Aware of the Traps of Condition Variables“.

    When you only need a one-time notification, such as in the previous program, promises and futures are a better choice than condition variables. Promises and futures cannot be victims of spurious or lost wakeups.

    Promises and Futures

    A promise can send a value, an exception, or a notification to its associated future. Let me use a promise and a future to refactor the previous workflow. Here is the same workflow using a promise/future pair.

     

    // threadSynchronisationPromiseFuture.cpp
    
    #include <iostream>
    #include <future>
    #include <thread>
    #include <vector>
    
    std::vector<int> myVec{};
    
    void prepareWork(std::promise<void> prom) {
    
        myVec.insert(myVec.end(), {0, 1, 0, 3});
        std::cout << "Sender: Data prepared."  << std::endl;
        prom.set_value();                                     // (1)
    
    }
    
    void completeWork(std::future<void> fut){
    
        std::cout << "Worker: Waiting for data." << std::endl;
        fut.wait();                                           // (2)
        myVec[2] = 2;
        std::cout << "Waiter: Complete the work." << std::endl;
        for (auto i: myVec) std::cout << i << " ";
        std::cout << std::endl;
        
    }
    
    int main() {
    
        std::cout << std::endl;
    
        std::promise<void> sendNotification;
        auto waitForNotification = sendNotification.get_future();
    
        std::thread t1(prepareWork, std::move(sendNotification));
        std::thread t2(completeWork, std::move(waitForNotification));
    
        t1.join();
        t2.join();
    
        std::cout << std::endl;
      
    }
    

     

    When you study the workflow, you recognize that the synchronization is reduced to its essential parts: prom.set_value() (1)  and fut.wait() (2). There is neither a need to use locks or mutexes nor is there a need to use a predicate to protect against spurious or lost wakeups. I skip the screenshot to this run because it is essentially the same, such as in the case of the previous run with condition variables.

    One downside to using promises and futures is that they can only be used once. Here are my previous posts on promises and futures, often called tasks.

    If you want to communicate more than once, use condition variables or atomics.

    std::atomic_flag

    std::atomic_flag in C++11 has a simple interface. Its member function clear lets you set its value to false, with test_and_set to true. In case you use test_and_set you get the old value back. ATOMIC_FLAG_INIT enables it to initialize the std::atomic_flag to false. std::atomic_flag has two exciting properties. 

    std::atomic_flag is

    • the only lock-free atomic.
    • the building block for higher thread abstractions.

    The remaining more powerful atomics can provide their functionality by using a mutex. That is according to the C++ standard. So these atomics have a member function is_lock_free .On the popular platforms, I always get the answer true. But you should be aware of that. Here are more details on the capabilities of std::atomic_flag C++11.

    Now, I jump directly from C++11 to C++20. With C++20, std::atomic_flag atomicFlag support new member functions: atomicFlag.wait(), atomicFlag.notify_one(), and atomicFlag.notify_all(). The member functions notify_one or notify_all notify one or all of the waiting atomic flags. atomicFlag.wait(boo) needs a boolean boo. The call atomicFlag.wait(boo) blocks until the subsequent notification or spurious wakeup. It checks then if the value  atomicFlag is equal to boo and unblocks if not. The value boo serves as a kind of predicate.

    Additionally, to C++11, default construction of a std::atomic_flag sets it in its false state, and you can ask for the value of the std::atomic flag via atomicFlag.test(). With this knowledge, it’s pretty easy to refactor previous programs using a std::atomic_flag.

     

    // threadSynchronisationAtomicFlag.cpp
    
    #include <iostream>
    #include <atomic>
    #include <thread>
    #include <vector>
    
    std::vector<int> myVec{};
    
    std::atomic_flag atomicFlag{};
    
    void prepareWork() {
    
        myVec.insert(myVec.end(), {0, 1, 0, 3});
        std::cout << "Sender: Data prepared."  << std::endl;
        atomicFlag.test_and_set();                             // (1)
        atomicFlag.notify_one();   
    
    }
    
    void completeWork() {
    
        std::cout << "Worker: Waiting for data." << std::endl;
        atomicFlag.wait(false);                                // (2)
        myVec[2] = 2;
        std::cout << "Waiter: Complete the work." << std::endl;
        for (auto i: myVec) std::cout << i << " ";
        std::cout << std::endl;
        
    }
    
    int main() {
    
        std::cout << std::endl;
    
        std::thread t1(prepareWork);
        std::thread t2(completeWork);
    
        t1.join();
        t2.join();
    
        std::cout << std::endl;
      
    }

     

    The thread preparing the work (1) sets the atomicFlag to true and sends the notification. The thread completing the work waits for the notification. It is only unblocked if atomicFlag is equal to true.

    Here are a few runs of the program with the Microsoft Compiler.

    threadSynchronisationAtomicFlag

    I’m not sure if I would use a future/promise pair or a std::atomic_flag for such a simple thread synchronization workflow. Both are thread-safe by design and require no protection mechanism so far. Promise and promise are easier to use but std::atomic_flag is probably faster. I’m sure I would not use a condition variable if possible.

    What’s next?

    When you create a more complicated thread synchronization workflow, such as a ping/pong game, a promise/future pair is no option. You have to use condition variables or atomics for multiple synchronizations. In my next post, I will implement a ping/pong game using condition variables and a std::atomic_flag and measure their performance.

    Short Break

    I take a short Christmas break and publish the following post on the 11.th of January. To learn more about C++20, read my new book at Leanpub to C++20.

     

     

     

     

    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,and Matt Godbolt.

    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 *