Atomic References with C++20

Contents[Show]

Atomics receives a few important extensions in C++20. Today, I start with the new data type std::atomic_ref.

 TimelineCpp20CoreLanguage

The type std::atomic_ref applies atomic operations to its referenced object.

std::atomic_ref

Concurrent writing and reading using a std::atomic_ref is no data race. The lifetime of the referenced object must exceed the lifetime of the std::atomic_ref. Accessing a subobject of the referenced object with a std::atomic_ref is not well-defined.

Motivation

You may think that using a reference inside an atomic would do the job. Unfortunately not.

In the following program, I have a class ExpensiveToCopy, which includes a counter. The counter is concurrently incremented by a few threads. Consequently, counter has to be protected.

 

// atomicReference.cpp

#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

struct ExpensiveToCopy {
    int counter{};
};
 
int getRandom(int begin, int end) {            // (6)

    std::random_device seed;        // initial seed
    std::mt19937 engine(seed());    // generator
    std::uniform_int_distribution<> uniformDist(begin, end);

    return uniformDist(engine);
}
 
void count(ExpensiveToCopy& exp) {                      // (2)
    
    std::vector<std::thread> v;
    std::atomic<int> counter{exp.counter};              // (3)
    
    for (int n = 0; n < 10; ++n) {                      // (4)
        v.emplace_back([&counter] {
            auto randomNumber = getRandom(100, 200);    // (5)
            for (int i = 0; i < randomNumber; ++i) { ++counter; }
        });
    }
    
    for (auto& t : v) t.join();

}

int main() {

    std::cout << std::endl;

    ExpensiveToCopy exp;              // (1)
    count(exp);
    std::cout << "exp.counter: " << exp.counter << '\n';

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

 

exp (1) is the expensive-to-copy object. For performance reasons, the function count (2) takes exp by reference. count initializes the std::atomic<int> with exp.counter (3). The following lines create 10 threads (4), each performing the lambda expression, which takes counter by reference. The lambda expression gets a random number between 100 and 200 (5) and increments the counter exactly as often. The function getRandom (6) start with an initial seed and creates via the random number generator Mersenne Twister a uniform distributed number.

In the end, the exp.counter (7) should have an approximate value of 1500 because of the ten threads increments on average 150 times. Executing the program on the Wandbox online compiler gives me a surprising result.

 

atomicReference

The counter is 0. What is happening? The issue is in line (3). The initialization in the expression std::atomic<int> counter{exp.counter} creates a copy. The following small program exemplifies the issue.

 

// atomicRefCopy.cpp

#include <atomic>
#include <iostream>

int main() {
    
    std::cout << std::endl;

    int val{5};
    int& ref = val;                     // (2)
    std::atomic<int> atomicRef(ref);
    ++atomicRef;                        // (1)
    std::cout << "ref: " << ref << std::endl;
    std::cout << "atomicRef.load(): " << atomicRef.load() << std::endl;
    
    std::cout << std::endl;

}

The increment operation (1) does not address the reference ref (2). The value of ref is not changed.

atomicRefCopy

 

Replacing the std::atomic<int> counter{exp.counter} with std::atomic_ref<int> counter{exp.counter} solves the issue:

 

// atomicReference.cpp

#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

struct ExpensiveToCopy {
    int counter{};
};
 
int getRandom(int begin, int end) {

    std::random_device seed;        // initial randomness
    std::mt19937 engine(seed());    // generator
    std::uniform_int_distribution<> uniformDist(begin, end);

    return uniformDist(engine);
}
 
void count(ExpensiveToCopy& exp) {
    
    std::vector<std::thread> v;
    std::atomic_ref<int> counter{exp.counter};
    
    for (int n = 0; n < 10; ++n) {
        v.emplace_back([&counter] {
            auto randomNumber = getRandom(100, 200);
            for (int i = 0; i < randomNumber; ++i) { ++counter; }
        });
    }
    
    for (auto& t : v) t.join();

}

int main() {

    std::cout << std::endl;

    ExpensiveToCopy exp;
    count(exp);
    std::cout << "exp.counter: " << exp.counter << '\n';

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

 

Now, the value of counter is as expected:

atomicRef

To be Atomic or Not to be Atomic

You may ask me why I didn't make the counter atomic in the first place:

 

struct ExpensiveToCopy {
    std::atomic<int> counter{};
};

 

Of course, this is a valid approach, but this approach has a big downside. Each access of the counter is synchronized, and synchronization is not for free. On the contrary, using a std::atomic_ref<int> counter lets you explicitly control when you need atomic access to the counter. Maybe, most of the time, you only want to read the value of the counter. Consequently, defining it as an atomic is pessimization.

Let me conclude my post with a few more details to the class template std::atomic_ref.

Specializations of std::atomic_ref

You can specialize std::atomic_ref for user-defined type, use partially specializations for pointer types or full specializations for arithmetic types such as integral or floating-point types.

Primary Template

The primary template std::atomic_ref can be instantiated with a trivially copyable type T. Trivially copyable types are either scalar types (arithmetic types, enum's, pointers, member pointers, or std::nullptr_t's), or trivially copyable classes and arrays of scalar types

Partial Specializations for Pointer Types

The standard provides partial specializations for a pointer type: std::atomic_ref<t*>.

Specializations for Arithmetic Types

The standard provides specialization for the integral and floating-point types: std::atomic_ref<arithmetic type>.

  • Character types: char, char8_t (C++20), char16_t, char32_t, and wchar_t
  • Standard signed integer types: signed char, short, int, long, and long long
  • Standard unsigned integer types: unsigned char, unsigned short, unsigned int, unsigned long, and unsigned long long
  • Additional integer types, defined in the header <cstdint>
  • Standard floating-point types: float, double, and long double

All Atomic Operations

First, here is the list of all operations on std::atomic_ref.

atomicRefFunctions

The composite assignment operators (+=, -=, |=, &=, or ^= ) return the new value; the fetch variations return the old value. The compare_exchange_strong and compare_exchange_weak perform an atomic exchange if equal and an atomic load if not. They return true in the success case, otherwise false. Each function supports an additional memory-ordering argument. The default is sequential consistency.

Of course, not all operations are available on all types referenced by std::atomic_ref. The table shows the list of all atomic operations depending on the type referenced by std::atomic_ref.

operationsAtomicRef

When you study the last two tables carefully, you notice that you can use std::atomic_ref to synchronize threads.

What's next?

std::atomic and std::atomic_ref support in C++20 member functions notify_one, notify_all, and wait. The three functions provide a convenient way to synchronize threads. In my next post, I will have a closer look at std::atomic and, in particular, the thread synchronisation with std::atomic's 

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, lennonli, Pramod Tikare Muralidhara, and Peter Ware.

 

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

 

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++,

RainerGrimmSmall

 

Tags: atomics

Comments   

0 #1 Pedro 2020-12-20 04:05
Regarding the approach of simply making the counter atomic, wouldn't the always synchronized access downside be beaten by using a relaxed memory ordering when you don't need thread safety?
Quote
0 #2 Rainer Grimm 2020-12-25 18:54
Quoting Pedro:
Regarding the approach of simply making the counter atomic, wouldn't the always synchronized access downside be beaten by using a relaxed memory ordering when you don't need thread safety?

When you don't need atomicity just use an int.
The performance difference of an atomic using sequenial-consistency and relaxed ordering is on x86 probably minimal. This may change on other architectures.
On the end, you have to measure.
Quote

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 6259

Yesterday 7916

Week 40049

Month 153512

All 5450616

Currently are 171 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments