In the book "Is Parallel Programming Hard, And, If So,What Can You Do About It?", the author uses several macros that I don't understand what they actually do.
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
#define READ_ONCE(x) \
({ typeof(x) ___x = ACCESS_ONCE(x); ___x; })
#define WRITE_ONCE(x, val) \
do { ACCESS_ONCE(x) = (val); } while (0)
I don't understand what ACCESS_ONCE
macro does and why it needs to cast and de-reference from and to an object of type volatile pointer.
and what is the usage of __x
at the end of READ_ONCE
macro?
In the following also there are some usages of these macros that (again) i don't understand.
Here is a list of situations allowing plain loads and stores for some accesses to a given variable, while requiring markings (such as READ_ONCE() and WRITE_ONCE()) for other accesses to that same variable:
- A shared variable is only modified by a given owning CPU or thread, but is read by other CPUs or threads. All stores must use WRITE_ONCE(). The owning CPU or thread may use plain loads. Everything else must use READ_ONCE() for loads.
- A shared variable is only modified while holding a given lock, but is read by code not holding that lock. All stores must use WRITE_ONCE(). CPUs or threads holding the lock may use plain loads. Everything else must use READ_ONCE() for loads.
- A shared variable is only modified while holding a given lock by a given owning CPU or thread, but is read by other CPUs or threads or by code not holding that lock. All stores must use WRITE_ONCE(). The owning CPU or thread may use plain loads, as may any CPU or thread holding the lock. Everything else must use READ_ONCE() for loads.
- A shared variable is only accessed by a given CPU or thread and by a signal or interrupt handler running in that CPU’s or thread’s context. The handler can use plain loads and stores, as can any code that has prevented the handler from being invoked, that is, code that has blocked signals and/or interrupts. All other code must use READ_ONCE() and WRITE_ONCE().
- A shared variable is only accessed by a given CPU or thread and by a signal or interrupt handler running in that CPU’s or thread’s context, and the handler always restores the values of any variables that it has written before return. The handler can use plain loads and stores, as can any code that has prevented the handler from being invoked, that is, code that has blocked signals and/or interrupts. All other code can use plain loads, but must use WRITE_ONCE() to prevent store tearing, store fusing, and invented stores.
First of all how can we use these macros to have simultaneous access to memory? AFAIK the volatile keyword is not safe for concurrent memory access.
In the item number 1, how can we use READ_ONCE
and WRITE_ONCE
to access a shared variable without data race?
And in item number 2, why does he use WRITE_ONCE
macro when write is only allowed by holding lock. and why doesn't read need to hold the lock?
volatile
does nothing to prevent race conditions and Undefined Behaviour when sharing data across threads. You should probably be using the standard<atomic>
library instead. – Bridewellstd::atomic
) variable outside of a lock whilst another thread is writing it is broken, no matter how many useless macros are used. – Olianalinux-kernel
was tagged. In which case you need to look up the multithreaded requirements of that environment. – OlianaREAD_ONCE
andWRITE_ONCE
are usable for C89 standard, where atomic doesn't exist even as a compiler's extension. These macros provide facilities similar to ones, which are provided bymemory_order_relaxed
loads and stores in C11. Their implementation viavolitile
is correct forgcc
compiler, which is the only one supported for Linux kernel. – Echino___x
is reserved for use by the implementation. It has no business being in that code. – UnmannerlyAtomic Operations (GCC Classic)
chapter - basically low-level workarounds for not provided builtin functionality (the way they implement them in Linux kernel), and the next chapterAtomic Operations (C11)
explains there are builtins provided by modern compiler. – CivilianC
"...Note that volatile variables are not suitable for communication between threads; they do not offer atomicity, synchronization, or memory ordering. A read from a volatile variable that is modified by another thread without synchronization or concurrent modification from two unsynchronized threads is undefined behavior due to a data race...." en.cppreference.com/w/c/language/volatile – BeldingC++
"...This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order). ...." en.cppreference.com/w/cpp/language/cv – Belding___x
so that it wouldn't clash with the parameter. Also, the Linux kernel from which those macros are taken does not use the standard library, and those macros are part of the kernel's "library". – Crocket