在没有易失性、std::atomic、信号量、互斥量和自旋锁的情况下访问共享内存?
Accessing Shared Memory Without Volatile, std::atomic, semaphore, mutex, and spinlock?
我读了。
答案有趣地指出:
You do in fact need to modify your code to not use C library functions
on volatile buffers. Your options include:
- Write your own alternative to the C library function that works with volatile buffers.
- Use a proper memory barrier.
我很好奇#2 怎么可能。假设 2 个(单线程)进程在 CentOS 7 上使用 shm_open()
+ memcpy()
到 create/open 相同的共享内存。
我正在使用 gcc/g++ 7 和 x86-64。
您可以使用 a process-shared mutex. 或信号量。
NAME
pthread_mutexattr_getpshared
, pthread_mutexattr_setpshared
- get
and set the process-shared attribute
SYNOPSIS
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
int pshared); [Option End]
DESCRIPTION
The pthread_mutexattr_getpshared()
function shall obtain the value
of the process-shared attribute from the attributes object referenced
by attr
. The pthread_mutexattr_setpshared()
function shall set the
process-shared attribute in an initialized attributes object
referenced by attr
.
The process-shared attribute is set to PTHREAD_PROCESS_SHARED
to
permit a mutex to be operated upon by any thread that has access to
the memory where the mutex is allocated, even if the mutex is
allocated in memory that is shared by multiple processes. If the
process-shared attribute is PTHREAD_PROCESS_PRIVATE
, the mutex shall
only be operated upon by threads created within the same process as
the thread that initialized the mutex; if threads of differing
processes attempt to operate on such a mutex, the behavior is
undefined. The default value of the attribute shall be
PTHREAD_PROCESS_PRIVATE
.
有关进程共享互斥体的示例,请参阅 Condition Variable in Shared Memory - is this code POSIX-conformant?。
NAME
sem_init
- initialize an unnamed semaphore (REALTIME)
SYNOPSIS
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned value); [Option End]
DESCRIPTION
The sem_init()
function shall initialize the unnamed semaphore
referred to by sem
. The value of the initialized semaphore shall be
value. Following a successful call to sem_init()
, the semaphore may
be used in subsequent calls to sem_wait()
, sem_timedwait()
,
sem_trywait()
, sem_post()
, and sem_destroy()
. This semaphore
shall remain usable until the semaphore is destroyed.
If the pshared
argument has a non-zero value, then the semaphore is
shared between processes; in this case, any process that can access
the semaphore sem
can use sem
for performing sem_wait()
,
sem_timedwait()
, sem_trywait()
, sem_post()
, and sem_destroy()
operations.
有关进程共享信号量的示例,请参阅 How to share semaphores between processes using shared memory。
直接回答您的直接问题:使用标准内存屏障 - 将 while 循环更改为:
while (strncmp((char *) mem, "exit", 4) != 0)
atomic_thread_fence(memory_order_acquire);
(请注意,这是 C。您已将您的问题标记为 C++,而您所指的原始 post 是 C。但是,等效的 C++ 看起来非常相似)。
粗略地说,memory_order_acquire
意味着您想查看其他线程(或在本例中为其他进程)所做的更改。这似乎已经足够了,在我进行的一些简单实验中使用当前的编译器,但是 技术上可能还不够 如果没有原子操作。完整的解决方案将使用原子加载重新实现 strncmp
函数。
严格来说,你不应该在易失性缓冲区上使用 strncmp
之类的东西(即使有内存屏障,这几乎肯定会引发未定义的行为,尽管我想你永远不会遇到当前的问题编译器)。
还有很多更好的方法可以解决您链接的 post 中描述的问题。特别是,对于这种情况,首先使用共享内存意义不大;一个简单的管道将是一个更好的解决方案。
滚动你自己的编译器内存屏障,告诉编译器所有全局变量可能已经被异步修改。
在 C++11 及更高版本中,该语言定义了一个内存模型,该模型指定非原子变量上的数据竞争是未定义的行为。因此,尽管这在现代编译器上仍然有效,但我们应该只讨论 C++03 及更早版本。在 C++11 之前,您必须自己动手,或使用 pthreads 库函数或任何其他库。
相关:
在 GNU C 中 asm("" ::: "memory")
是一个编译器内存屏障。 在 x86 上,一个强有序的架构,这本身就给了你 acq_rel 语义,因为只有一种 运行x86 可以做的时间重新排序是 StoreLoad。
优化器将其视为对非内联函数的函数调用:假定此函数外部 任何东西 可能有指针指向的任何内存都被修改。参见 。 (没有输出的 GNU C 扩展 asm 语句隐含 volatile
,所以 asm volatile("" ::: "memory")
更明确但等效。)
另请参阅 http://preshing.com/20120625/memory-ordering-at-compile-time/ 了解有关编译器障碍的更多信息。但请注意,这不仅会阻止重新排序,还会阻止优化,例如将值保存在循环中的寄存器中。
例如像 while(shared_var) {}
这样的自旋循环可以编译成 if(shared_var) infinite_loop;
,但是我们可以通过屏障来阻止它:
void spinwait(int *ptr_to_shmem) {
while(shared_var) {
asm("" ::: "memory");
}
}
gcc -O3 for x86-64 (on the Godbolt compiler explorer) 将其编译为看起来像源代码的 asm,而无需将负载提升到循环之外:
# gcc's output
spinwait(int*):
jmp .L5 # gcc doesn't check or know that the asm statement is empty
.L3:
#APP
# 3 "/tmp/compiler-explorer-compiler118610-54-z1284x.occil/example.cpp" 1
#asm comment: barrier here
# 0 "" 2
#NO_APP
.L5:
mov eax, DWORD PTR [rdi]
test eax, eax
jne .L3
ret
asm
语句仍然是一个可变的 asm 语句,它必须 运行 与 C 抽象机中的循环体 运行 一样多。 GCC 跳过空的 asm 语句到达循环底部的条件,以确保在 运行 执行(空的)asm 语句之前检查条件。我在 asm 模板中添加了一条 asm 注释,以查看它在编译器为整个函数生成的 asm 中的最终位置。我们可以通过在 C 源代码中编写 do{}while()
循环来避免这种情况。 ().
除此之外,它与我们使用 std::atomic_int
或 volatile
得到的 asm 相同。 (参见 Godbolt link)。
没有障碍物,它确实能吊起负载:
# clang6.0 -O3
spinwait_nobarrier(int*): # @spinwait_nobarrier(int*)
cmp dword ptr [rdi], 0
je .LBB1_2
.LBB1_1: #infinite loop
jmp .LBB1_1
.LBB1_2: # jump target for 0 on entry
ret
没有任何特定于编译器的东西,您实际上可以 使用 一个非内联函数来击败优化器,但您可能必须将它放在库中才能击败 link-时间优化。仅仅另一个源文件是不够的。所以你最终需要一个特定于系统的 Makefile 或其他什么。 (而且它有 运行 时间开销)。
我读了
答案有趣地指出:
You do in fact need to modify your code to not use C library functions on volatile buffers. Your options include:
- Write your own alternative to the C library function that works with volatile buffers.
- Use a proper memory barrier.
我很好奇#2 怎么可能。假设 2 个(单线程)进程在 CentOS 7 上使用 shm_open()
+ memcpy()
到 create/open 相同的共享内存。
我正在使用 gcc/g++ 7 和 x86-64。
您可以使用 a process-shared mutex. 或信号量。
NAME
pthread_mutexattr_getpshared
,pthread_mutexattr_setpshared
- get and set the process-shared attributeSYNOPSIS
#include <pthread.h> int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared); [Option End]
DESCRIPTION
The
pthread_mutexattr_getpshared()
function shall obtain the value of the process-shared attribute from the attributes object referenced byattr
. Thepthread_mutexattr_setpshared()
function shall set the process-shared attribute in an initialized attributes object referenced byattr
.The process-shared attribute is set to
PTHREAD_PROCESS_SHARED
to permit a mutex to be operated upon by any thread that has access to the memory where the mutex is allocated, even if the mutex is allocated in memory that is shared by multiple processes. If the process-shared attribute isPTHREAD_PROCESS_PRIVATE
, the mutex shall only be operated upon by threads created within the same process as the thread that initialized the mutex; if threads of differing processes attempt to operate on such a mutex, the behavior is undefined. The default value of the attribute shall bePTHREAD_PROCESS_PRIVATE
.
有关进程共享互斥体的示例,请参阅 Condition Variable in Shared Memory - is this code POSIX-conformant?。
NAME
sem_init
- initialize an unnamed semaphore (REALTIME)SYNOPSIS
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned value); [Option End]
DESCRIPTION
The
sem_init()
function shall initialize the unnamed semaphore referred to bysem
. The value of the initialized semaphore shall be value. Following a successful call tosem_init()
, the semaphore may be used in subsequent calls tosem_wait()
,sem_timedwait()
,sem_trywait()
,sem_post()
, andsem_destroy()
. This semaphore shall remain usable until the semaphore is destroyed.If the
pshared
argument has a non-zero value, then the semaphore is shared between processes; in this case, any process that can access the semaphoresem
can usesem
for performingsem_wait()
,sem_timedwait()
,sem_trywait()
,sem_post()
, andsem_destroy()
operations.
有关进程共享信号量的示例,请参阅 How to share semaphores between processes using shared memory。
直接回答您的直接问题:使用标准内存屏障 - 将 while 循环更改为:
while (strncmp((char *) mem, "exit", 4) != 0)
atomic_thread_fence(memory_order_acquire);
(请注意,这是 C。您已将您的问题标记为 C++,而您所指的原始 post 是 C。但是,等效的 C++ 看起来非常相似)。
粗略地说,memory_order_acquire
意味着您想查看其他线程(或在本例中为其他进程)所做的更改。这似乎已经足够了,在我进行的一些简单实验中使用当前的编译器,但是 技术上可能还不够 如果没有原子操作。完整的解决方案将使用原子加载重新实现 strncmp
函数。
严格来说,你不应该在易失性缓冲区上使用 strncmp
之类的东西(即使有内存屏障,这几乎肯定会引发未定义的行为,尽管我想你永远不会遇到当前的问题编译器)。
还有很多更好的方法可以解决您链接的 post 中描述的问题。特别是,对于这种情况,首先使用共享内存意义不大;一个简单的管道将是一个更好的解决方案。
滚动你自己的编译器内存屏障,告诉编译器所有全局变量可能已经被异步修改。
在 C++11 及更高版本中,该语言定义了一个内存模型,该模型指定非原子变量上的数据竞争是未定义的行为。因此,尽管这在现代编译器上仍然有效,但我们应该只讨论 C++03 及更早版本。在 C++11 之前,您必须自己动手,或使用 pthreads 库函数或任何其他库。
相关:
在 GNU C 中 asm("" ::: "memory")
是一个编译器内存屏障。 在 x86 上,一个强有序的架构,这本身就给了你 acq_rel 语义,因为只有一种 运行x86 可以做的时间重新排序是 StoreLoad。
优化器将其视为对非内联函数的函数调用:假定此函数外部 任何东西 可能有指针指向的任何内存都被修改。参见 volatile
,所以 asm volatile("" ::: "memory")
更明确但等效。)
另请参阅 http://preshing.com/20120625/memory-ordering-at-compile-time/ 了解有关编译器障碍的更多信息。但请注意,这不仅会阻止重新排序,还会阻止优化,例如将值保存在循环中的寄存器中。
例如像 while(shared_var) {}
这样的自旋循环可以编译成 if(shared_var) infinite_loop;
,但是我们可以通过屏障来阻止它:
void spinwait(int *ptr_to_shmem) {
while(shared_var) {
asm("" ::: "memory");
}
}
gcc -O3 for x86-64 (on the Godbolt compiler explorer) 将其编译为看起来像源代码的 asm,而无需将负载提升到循环之外:
# gcc's output
spinwait(int*):
jmp .L5 # gcc doesn't check or know that the asm statement is empty
.L3:
#APP
# 3 "/tmp/compiler-explorer-compiler118610-54-z1284x.occil/example.cpp" 1
#asm comment: barrier here
# 0 "" 2
#NO_APP
.L5:
mov eax, DWORD PTR [rdi]
test eax, eax
jne .L3
ret
asm
语句仍然是一个可变的 asm 语句,它必须 运行 与 C 抽象机中的循环体 运行 一样多。 GCC 跳过空的 asm 语句到达循环底部的条件,以确保在 运行 执行(空的)asm 语句之前检查条件。我在 asm 模板中添加了一条 asm 注释,以查看它在编译器为整个函数生成的 asm 中的最终位置。我们可以通过在 C 源代码中编写 do{}while()
循环来避免这种情况。 (
除此之外,它与我们使用 std::atomic_int
或 volatile
得到的 asm 相同。 (参见 Godbolt link)。
没有障碍物,它确实能吊起负载:
# clang6.0 -O3
spinwait_nobarrier(int*): # @spinwait_nobarrier(int*)
cmp dword ptr [rdi], 0
je .LBB1_2
.LBB1_1: #infinite loop
jmp .LBB1_1
.LBB1_2: # jump target for 0 on entry
ret
没有任何特定于编译器的东西,您实际上可以 使用 一个非内联函数来击败优化器,但您可能必须将它放在库中才能击败 link-时间优化。仅仅另一个源文件是不够的。所以你最终需要一个特定于系统的 Makefile 或其他什么。 (而且它有 运行 时间开销)。