C++ Logo

std-discussion

Advanced search

Re: Deprecation of volatile - Implications for exsiting code

From: J Decker <d3ck0r_at_[hidden]>
Date: Sun, 24 Nov 2024 11:35:19 -0800
On Sun, Nov 24, 2024 at 9:52 AM Nate Eldredge <nate_at_[hidden]>
wrote:

> I'm afraid your ideas about `volatile` with respect to multi-threading are
> about 14 years out of date. You're reading an object in one thread while
> it's being concurrently written in another, and it's not `std::atomic`.
> Since C++11, that's a data race and your program has undefined behavior.
> Under the standard, `volatile` doesn't save you; `std::atomic` is the only
> conformant way to do it. (And `std::atomic` does in fact achieve
> everything you want here; for instance, it inhibits optimizations like
> "read the value once and then never read it again.")
>
>
I'm reading a pointer, not an object, so when is a register sized
load/store/move not atomic?

"It is unclear how to use such weak guarantees in portable code, except in
a few rare instances, which we list below." from
"In case you wonder why volatile does not work for multi-threading, see
   https://wg21.link/N2016 "

... 1) I am one of those instances 2) sorry it wasn't clear, it's quite
clear to me, but then in the 90's I had the advantage of a logic analyzer
connected to every signal pin of a 386 processor (which was before there
was in-cpu cache even), and intimately understand word tear if such
register sized values were not on a register-sized boundary. can't see how
processors would devolve into a stream of bits or something that volatile
for 'read always' and 'execute in the order I specified in the code'
wouldn't cover... the first bit would either bit the previous or the next
and all subsequently - maybe there's a grey-code possibility, but then
that's something the hardware is doing to you, and maybe you should lock at
a higher level and use a different object for the list of data.

In many cases, the thread-safety of my atomics implementation is often
unused - on reflection in regards to this issue, the lock is really just a
write-lock, with lock-free reads; and in many cases isn't strictly needed
to be volatile, because there's a code lock around it; but then volatile
still need to be specified in the type because... PLIST is a simil.ar
type, but stores only pointers to things, or pointer-sized values.

#define ever ;;
CRITICALSECTION cs; // assume inited...
void f( PLIST **outList ) {

PLIST list = NULL;
outList[0] = &list;
EnterCriticalSection( &cs );
for( ever ) {
    if( !list ) { // there's nothing in this code path that would change
this value
                   // so it's a candidate for read-never optimization; even
without any locks on the object.
      LeaveCriticalSection( &cs ); sleep( forever ); EnterCriticalSection(
&cs ); continue;
    }
    /* do something with the data in the list */
    // assuming you CAN get here, because the read-again of list didn't
happen.
}

}


void g( void ) {
   PLIST *useList;
   uinitptr_t data = 3;
   PTHREAD thread = ThreadTo( f, &useList );
   AddLink( useList, &data );
    WakeThread( thread );
   // and later doing other work with this list... which in many cases ends
up being an address of the volatile
   // but not ALL cases...
}


> It's possible that your implementation provides some guarantees about
> `volatile` in multithreading beyond what the standard gives, but if so,
> that's between you and them; nothing to do with ISO C++.
>
> Now, this doesn't directly answer your question about `volatile` on
> parameters and return types, but it does rather invalidate the use case you
> proposed.
>
> The intended uses for `volatile` in this day and age are things like
> memory-mapped I/O, where your goal is to just make the machine perform a
> single load or store instruction. (Again, you may *think* that's exactly
> what you want for concurrent multithread access, but the standard doesn't
> promise that it will work.) So if you can propose an example of that kind,
> we could get back on track. However, for memory-mapped I/O, you almost
> never want to define an actual object of `volatile`-qualified type. You
> would work through pointers to such types, but the pointers themselves
> would not be `volatile`.
>

'but the standard doesn't promise that it will work' interpretations of the
standard that mixed a bunch of correlated but maybe not causally related
things together to compilcate the issue.... maybe better wording would make
it less subject to interpretation so you don't have to argue with each
implementation that decides A is tied to B, and decides to break (well,
they didn't always have volatile in the type either, but as optimizers got
too eager and broke too much, they just inherited them probably about 15
years ago so right when volatile became unusable for all the things that do
work and did work. But apparently this is around the time this is when
they start telling me that 'the sky is green, not blue' what YOU(me, from
you) see and have working, isn't actually working.




>
> On Nov 23, 2024, at 19:03, J Decker via Std-Discussion <
> std-discussion_at_[hidden]> wrote:
>
>
> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1152r0.html#parret
>
>
> I have some datatype, which is a volatile list. What makes it volatile is
> that other threads can change the values associated with the list without
> any other threads being aware of the change. Primarily, what happens, when
> the objects are not defined as volatile, the optimizers can read the value
> early in a function, and then never read it again. The only meaning of
> volatile *I* knew of was 'read always'. Writes and locks and other
> unrelated things mentioned in the paper just confuse the issue. What I
> need is just a way to tell compiler optimizers 'hey don't be smart, if the
> code needs the value, get the value from the place specified.' Writes
> could be cached - but it's something the programmer would do. It(volatile)
> really has nothing to do atomics.
>
>

Received on 2024-11-24 19:35:37