Is using volatile on shared memory safe?
Asked Answered
D

1

3

Lets suppose following:

I have two processes on Linux / Mac OS.

I have mmap on shared memory (or in a file).

Then in both processes I have following:

struct Data{
   volatile int reload = 0; // using int because is more standard
   // more things in the future...
};
void *mmap_memory = mmap(...);
Data *data = static_cast<Data *>(mmap_memory); // suppose size is sufficient and all OK

Then in one of the processes I do:

//...
data->reload = 1;
//...

And in the other I do:

while(...){
    do_some_work();
    //...
    if (data->reload == 1)
        do_reload();
}

Will this be thread / inter process safe?

Idea is from here:
https://embeddedartistry.com/blog/2019/03/11/improve-volatile-usage-with-volatile_load-and-volatile_store/

Note:
This can not be safe with std::atomic<>, since it does not "promise" anything about shared memory. Also constructing/destructing from two different processes is not clear at all.

Dissemblance answered 24/8, 2022 at 11:32 Comment(8)
Do you unset data->reload in the second process?Jargon
@Jargon Even that's not sufficient if multiple threads/processes are running that while(...){...} loop or equivalent.Soloma
Dupe? Why is volatile not considered useful in multithreaded C or C++ programming? I'll let others decideSoloma
I would suggest using std::atomic_ref if C++20 is available to you.Hemicycle
@RichardCritten: You only need atomic_ref if you also want to more efficient non-atomic access to the same object at times. Given this question, it looks like they should just make reload and atomic<int> member of Data. Possibly init with placement-new to be slightly more efficient than data->reload.store(0, relaxed), or yeah that could be a reason to use atomic_ref, so you can assign to it cheaply while you know there's only one thread.Conto
@PeterCordes I was worried about implicit creation of atomic<int> in the shared memory and whether it was safe if the virtual addresses (from the 2 processes) were different. Does it need placement new from both processes for correct object life-times ? And if it does need placement new from 2 processes onto the same object is this ok. std::atomic_ref seems to make all the above questions moot.Hemicycle
@RichardCritten: I just commented on the answer: real implementations are address-free for lock-free atomics. Non-lock-free won't work for std::atomic or std::atomic_ref. Initialization is a non-issue in practice (on POSIX systems where you'd be using mmap); I forget what the standard has to say about it.Conto
If you're looking for standards, then volatile and atomic are both bad as you're not going to find ISO/IEEE-level promises about the behavior of either one relative to shared memory. That's just a gap in the standards as they exist. But if you're looking for reality, then atomic works fine and does what you want, while volatile does not. Any reasonable system will support that. Some may document it explicitly, others via their source code, others by "yes, everyone knows that is supposed to work".Frager
S
5

Will this be thread / inter process safe?

No.

From your own link:

One problematic and common assumption is that volatile is equivalent to “atomic”. This is not the case. All the volatile keyword denotes is that the variable may be modified externally, and thus reads/writes cannot be optimized.

Your code needs atomic access to the value. if (data->reload == 1) won't work if it reads some partial/intermediate value from data->reload.

And nevermind what happens if multiple threads do read 1 from data->reload - your posted code doesn't handle that at all.

Also see Why is volatile not considered useful in multithreaded C or C++ programming?

Soloma answered 24/8, 2022 at 11:42 Comment(9)
I know is not. But some sources suggest boost uses similar technic if running on mmap.Dissemblance
@Nick: std::atomic<> works on shared memory as long as the atomic type is_lock_free, because real implementations follow the standard's (non-normative) recommendation that it should be address-free. Are lock-free atomics address-free in practice?Conto
@Andrew: If the only values it ever has are 0 or 1, tearing is a non-issue in practice. The upper 3 bytes will always stay zero, and single bytes are inherently atomic on real CPUs. But yeah, there's no good reason not to use std::atomic with memory_order_relaxed for this, to get well-defined C++ semantics that will compile to the same asm you'd have gotten from volatile on real systems.Conto
In practice, at least GCC/clang do try to do volatile accesses with one instruction, making them atomic if they're sufficiently aligned and guaranteed by hardware. But again, no good reason to rely on that.Conto
Related: Two Different Processes With 2 std::atomic Variables on Same Address? shows how to static_assert that std::atomic<T> is lock-free and has the same size as T.Conto
@PeterCordes can you show me some working code? The thing I do not understand is - how I create atomic<int> via placement new, in same memory (mmap). also how to destroy it on same mmap-ed memory?Dissemblance
@Nick: Two Different Processes With 2 std::atomic Variables on Same Address? shows how to do that.Conto
@Nick: There is a lot of old code out there, and similarly old advice, that predates C11/C++11 atomics, and relies on volatile together with a lot of sketchy implementation-specific behavior. See https://mcmap.net/q/1634219/-atomic-equivalent-for-c89/… for a little more discussion. But such code/advice is not good guidance in the modern era. You have to consider your sources.Frager
@Nick: In real implementations of std::atomic, the constructor is pretty much trivial, just initializing the T member. And the destructor is trivial. Even non-lock-free atomics don't keep a mutex inside the atomic<T> itself (usually a global hash table of spinlocks, hashing on the address)Conto

© 2022 - 2024 — McMap. All rights reserved.