Nick
Nick

Reputation: 10539

Do I use atomic<> over shared memory correctly here

Suppose we have two processes.

Both use same mmap-ed memory region in order to pass some information, in this case uint16_t.

I read all kind of opinions why std::atomic<> should work, if it uses is_always_lock_free.

So I did this code. Code works as expected, but is this code thread / inter-process safe:

#include <cstdint>
#include <atomic>
#include <iostream>

#include <sys/mman.h>
#include <fcntl.h>

#include <unistd.h> // sleep
#include <time.h>

using I  = uint16_t;
using AI = std::atomic<I>;

static_assert(std::atomic<I>::is_always_lock_free);

constexpr size_t SIZE = sizeof(AI);
constexpr const char *NAME = "nmmm";

constexpr I STOP = 12345;

void error(const char *msg){
    std::cout << msg << '\n';
    exit(10);
}

int main(int argc, const char **argv){
    std::cout << sizeof(I) << ' ' << sizeof(AI) << '\n';

    int fd = shm_open(NAME,  O_RDWR | O_CREAT, 0644);
    if (fd < 0)
        error("shm_open");

    int t = ftruncate(fd, SIZE);
    if (t < 0)
        error("ftruncate");

    void *vmem = mmap(nullptr, SIZE, PROT_WRITE, MAP_SHARED, fd, 0);
    if (vmem == MAP_FAILED)
        error("mmap");

    std::cout << "All set up!" << ' ' << vmem << '\n';

    AI *ai = reinterpret_cast<AI *>(vmem);


    if (argc > 1){
        switch(argv[1][0]){
        case 'g':
        case 'G':
            ai->store(0, std::memory_order_relaxed);

            while(true){
                auto x = ai->load(std::memory_order_relaxed);

                std::cout << x << '\n';

                if (x == STOP)
                    break;

                sleep(1);
            }

        case 's':
        case 'S':
            ai->store(STOP, std::memory_order_relaxed);
            break;

        default:
            {
                srand(time(nullptr));

                I const x = rand() & 0xFFFF;

                std::cout << "set val to " << x << '\n';

                ai->store(x , std::memory_order_relaxed);
            }
            break;
        }
    }

    munmap(vmem, SIZE);
//  shm_unlink(NAME);
}

Excuse my mixing of C and C++ like using rand.

What you suppose to do it -

# console 1
./a.out g # start "Getter"

# console 2
./a.out x # set random value
# or
./a.out s # set "Stop" value, so process in console 1 stops.

What I am worrying:

Update

It appears, std::atomic<uint16_t> is POD. So casting is not UB (undefined behavour)

static_assert(std::is_pod_v<std::atomic<uint16_t> >);

Also it appears std::atomic<uint16_t> operations compiles to single assembly instructions:

https://gcc.godbolt.org/z/d6bK9jfza

Upvotes: 0

Views: 1032

Answers (2)

Nick
Nick

Reputation: 10539

Because C++ atomic are not very "clear" to use, you can use stdatomic.h.

https://en.cppreference.com/w/c/atomic/atomic_load

However I failed to compile example with these on gcc.

This is why I decided to use gcc builtin atomics:

https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html

These works on both gcc and clang on Linux. Did not checked on MacOS yes.

Here is the code, almost similar to what I posted before. Once again excuse C style code.

In order code to be similar to what stdatomic.h offers, I did two functions that call the builtin(s):

template<typename T>
void atomic_store(T *a, T val){
    return __atomic_store_n(a, val, __ATOMIC_RELAXED);
}

template<typename T>
void atomic_store(T &a, T val){
    return atomic_store(& a, val);
}

template<typename T>
T atomic_load(T *a){
    return __atomic_load_n(a, __ATOMIC_RELAXED);
}

template<typename T>
T atomic_load(T &a){
    return atomic_load(& a);
}

Those suppose to be good with all integral types such int, short, char, unsigned etc.

Here is the full code in case you decide to "dig" future.

#include <cstdio>

#include <sys/mman.h>
#include <fcntl.h>

#include <unistd.h> // sleep
#include <cstdlib>  // exit
#include <time.h>

using MyAtomicInt = int;

constexpr size_t SIZE = sizeof(MyAtomicInt);
constexpr const char *NAME = "nmmm";

constexpr MyAtomicInt STOP = 12345;

void error(const char *msg){
    printf("%s\n", msg);
    exit(10);
}



template<typename T>
void atomic_store(T *a, T val){
    return __atomic_store_n(a, val, __ATOMIC_RELAXED);
}

template<typename T>
void atomic_store(T &a, T val){
    return atomic_store(& a, val);
}

template<typename T>
T atomic_load(T *a){
    return __atomic_load_n(a, __ATOMIC_RELAXED);
}

template<typename T>
T atomic_load(T &a){
    return atomic_load(& a);
}



int main(int argc, const char **argv){
    int fd = shm_open(NAME,  O_RDWR | O_CREAT, 0644);
    if (fd < 0)
        error("shm_open");

    int t = ftruncate(fd, SIZE);
    if (t < 0)
        error("ftruncate");

    void *vmem = mmap(nullptr, SIZE, PROT_WRITE, MAP_SHARED, fd, 0);
    if (vmem == MAP_FAILED)
        error("mmap");

    printf("All set up! %p\n", vmem);

    MyAtomicInt *ai = reinterpret_cast<MyAtomicInt *>(vmem);


    if (argc > 1){
        switch(argv[1][0]){
        case 'g':
        case 'G':
            atomic_store(ai, 0);

            while(true){
                auto x = atomic_load(ai);

                printf("%d\n", x);

                if (x == STOP)
                    break;

                sleep(1);
            }

        case 's':
        case 'S':
            atomic_store(ai, STOP);
            break;

        default:
            {
                srand(time(nullptr));

                MyAtomicInt const x = rand() & 0xFFFF;

                printf("set val to %d\n", x);

                atomic_store(ai, x);
            }
            break;
        }
    }

    munmap(vmem, SIZE);
//  shm_unlink(NAME);
}

Upvotes: 1

Alex Guteniev
Alex Guteniev

Reputation: 13644

What I am worrying:

  • no placement new, just reinterpret cast + initial value set.
  • not a d-tor either.
  • why sizeof(uint16_t) is same as sizeof(std::atomic<uint16_t>) - is it always like this if is_always_lock_free ? Is std::atomic just a fancy Assembly instruction?

These assumtions are likely to be true, but formally these are leading to UB.

To avoid running into UB, use C++20 atomic_ref. Share plain uint16_t and use atomic_ref to it in processes.

With atomic_ref it is still not formally guaranteed to work in an interprocess way (no interprocess in the standard C++ at all), but you will not be running into the mentioned UB, so no concerns about lifetime or incorrect aliasing.

Note that you have to keep is_always_lock_free assertion. Without it, atomic_ref would use a locking mechanism to provide atomic semantic, this mechanism is likely to be private to a program / process.

Upvotes: 1

Related Questions