Synchronization with Atomics in C++20

Synchronization with Atomics in C++20

This is a cross-post from www.ModernesCpp.com.

Sender/receiver workflows are quite 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.

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 to condition variables or promise and futures. Therefore, I 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 (3). 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).

No alt text provided for this image

Condition variables have many inherent issues. For example, the receiver could be awakened without notification or could lose the notification. The first issue is known as spurious wakeup and the second as 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 output of the program, 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 details of the sender/receiver workflow and the traps of condition variables, read my previous posts "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. Promise 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 screen-shot to this run because it is essentially the same such in the case of the previous run with condition variables.

There is only one downside to using promises and futures: they can only be used once. Here are my previous posts to promises and futures, often just called tasks.

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

std::atomic_flag

std::atomic_flag in C++11 has a simple interface. It's member function clear enables you to 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 very interesting 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 in 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 next notification or spurious wakeup. It checks then if the value of 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 quite easy to refactor to 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.

No alt text provided for this image

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 only sure that 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 implement a ping/pong game using condition variables and a std::atomic_flag and measure their performance.

Short Break

I make a short Christmas break and publish the next post on the 11.th of January. In case you want to know 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, Marko, G Prvulovic, Reinhold Dr?ge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang G?rtner, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, Sudhakar Balagurusamy, lennonli, and Pramod Tikare Muralidhara.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, and Dendi Suhubdy

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

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

New

Contact Me

Modernes C++


要查看或添加评论,请登录

Rainer Grimm的更多文章

社区洞察

其他会员也浏览了