带栅栏的 SPSC 线程安全
SPSC thread safe with fences
我只希望我的代码尽可能简单且线程安全。
有 C11 原子
关于 ISO/IEC 9899/201X 草案的“7.17.4 Fences”部分
X and Y , both operating on some atomic object M, such that A is
sequenced before X, X modifies M, Y is sequenced before B, and Y reads
the value written by X or a value written by any side effect in the
hypothetical release sequence X would head if it were a release
operation.
此代码线程安全吗("w_i" 为 "object M")?
"w_i" 和 "r_i" 都需要声明为 _Atomic 吗?
如果只有 w_i 是 _Atomic,主线程可以在缓存中保留旧值 r_i 并认为队列未满(当它已满时)并写入数据吗?
如果我在没有 atomic_load 的情况下读取 atomic 会发生什么?
我做了一些测试,但我所有的尝试似乎都给出了正确的结果。
但是,我知道关于多线程我的测试并不真正正确:我 运行 我的程序好几次并查看结果。
即使 w_i 和 r_i 都没有被声明为 _Atomic,我的程序可以工作,但是对于 C11 标准来说只有栅栏是不够的,对吧?
typedef int rbuff_data_t;
struct rbuf {
rbuff_data_t * buf;
unsigned int bufmask;
_Atomic unsigned int w_i;
_Atomic unsigned int r_i;
};
typedef struct rbuf rbuf_t;
static inline int
thrd_tryenq(struct rbuf * queue, rbuff_data_t val) {
size_t next_w_i;
next_w_i = (queue->w_i + 1) & queue->bufmask;
/* if ring full */
if (atomic_load(&queue->r_i) == next_w_i) {
return 1;
}
queue->buf[queue->w_i] = val;
atomic_thread_fence(memory_order_release);
atomic_store(&queue->w_i, next_w_i);
return 0;
}
static inline int
thrd_trydeq(struct rbuf * queue, rbuff_data_t * val) {
size_t next_r_i;
/*if ring empty*/
if (queue->r_i == atomic_load(&queue->w_i)) {
return 1;
}
next_r_i = (queue->r_i + 1) & queue->bufmask;
atomic_thread_fence(memory_order_acquire);
*val = queue->buf[queue->r_i];
atomic_store(&queue->r_i, next_r_i);
return 0;
}
我调用这些函数如下:
主线程排队一些数据:
while (thrd_tryenq(thrd_get_queue(&tinfo[tnum]), i)) {
usleep(10);
continue;
}
其他线程出列数据:
static void *
thrd_work(void *arg) {
struct thrd_info *tinfo = arg;
int elt;
atomic_init(&tinfo->alive, true);
/* busy waiting when queue empty */
while (atomic_load(&tinfo->alive)) {
if (thrd_trydeq(&tinfo->queue, &elt)) {
sched_yield();
continue;
}
printf("Thread %zu deq %d\n",
tinfo->thrd_num, elt);
}
pthread_exit(NULL);
}
有asm fences
关于带有 lfence 和 sfence 的特定平台 x86,
如果我删除所有 C11 代码并只用
替换围栏
asm volatile ("sfence" ::: "memory");
和
asm volatile ("lfence" ::: "memory");
(我对这些宏的理解是:防止内存访问的编译器栅栏是 reoganized/optimized + 硬件栅栏)
例如,我的变量是否需要声明为 volatile?
我已经看到上面的环形缓冲区代码只有这些 asm fences 但没有原子类型,我真的很惊讶,我想知道这段代码是否正确。
我只是回复关于 C11 原子,平台细节太复杂,应该逐步淘汰。
C11 中线程之间的同步只能通过一些系统调用(例如 mtx_t
)和原子来保证。甚至不要尝试没有它。
也就是说,同步化 通过 原子进行,也就是说,副作用的可见性保证通过对原子的影响的可见性传播。例如,对于最简单的一致性模型,顺序模型,每当线程 T2 看到线程 T1 对原子变量 A 产生影响的修改时,线程 T1 中该修改之前的所有副作用对 T2 都是可见的。
所以并不是所有的共享变量都需要是原子的,你只需要确保你的状态通过原子正确传播。从这个意义上说,当您使用顺序或 acquire-release 一致性时,栅栏不会给您带来任何好处,它们只会使图片复杂化。
一些更一般的评论:
- 由于您似乎使用了顺序一致性模型,因此
默认情况下,原子操作的函数式写法(例如
atomic_load
) 是多余的。只是评估原子变量是
一模一样
- 我的印象是你也在尝试优化
在你发展的早期。我认为你应该做一个实现
首先,您可以证明其正确性。那么,当且仅当
你注意到性能问题,你应该开始考虑
优化。这种原子数据结构不太可能
是您应用程序的真正瓶颈。你必须有一个非常
大量的线程同时打击你的穷人
小原子变量,在这里看到一个可测量的瓶颈。
我只希望我的代码尽可能简单且线程安全。
有 C11 原子
关于 ISO/IEC 9899/201X 草案的“7.17.4 Fences”部分
X and Y , both operating on some atomic object M, such that A is sequenced before X, X modifies M, Y is sequenced before B, and Y reads the value written by X or a value written by any side effect in the hypothetical release sequence X would head if it were a release operation.
此代码线程安全吗("w_i" 为 "object M")?
"w_i" 和 "r_i" 都需要声明为 _Atomic 吗?
如果只有 w_i 是 _Atomic,主线程可以在缓存中保留旧值 r_i 并认为队列未满(当它已满时)并写入数据吗?
如果我在没有 atomic_load 的情况下读取 atomic 会发生什么?
我做了一些测试,但我所有的尝试似乎都给出了正确的结果。 但是,我知道关于多线程我的测试并不真正正确:我 运行 我的程序好几次并查看结果。
即使 w_i 和 r_i 都没有被声明为 _Atomic,我的程序可以工作,但是对于 C11 标准来说只有栅栏是不够的,对吧?
typedef int rbuff_data_t;
struct rbuf {
rbuff_data_t * buf;
unsigned int bufmask;
_Atomic unsigned int w_i;
_Atomic unsigned int r_i;
};
typedef struct rbuf rbuf_t;
static inline int
thrd_tryenq(struct rbuf * queue, rbuff_data_t val) {
size_t next_w_i;
next_w_i = (queue->w_i + 1) & queue->bufmask;
/* if ring full */
if (atomic_load(&queue->r_i) == next_w_i) {
return 1;
}
queue->buf[queue->w_i] = val;
atomic_thread_fence(memory_order_release);
atomic_store(&queue->w_i, next_w_i);
return 0;
}
static inline int
thrd_trydeq(struct rbuf * queue, rbuff_data_t * val) {
size_t next_r_i;
/*if ring empty*/
if (queue->r_i == atomic_load(&queue->w_i)) {
return 1;
}
next_r_i = (queue->r_i + 1) & queue->bufmask;
atomic_thread_fence(memory_order_acquire);
*val = queue->buf[queue->r_i];
atomic_store(&queue->r_i, next_r_i);
return 0;
}
我调用这些函数如下:
主线程排队一些数据:
while (thrd_tryenq(thrd_get_queue(&tinfo[tnum]), i)) {
usleep(10);
continue;
}
其他线程出列数据:
static void *
thrd_work(void *arg) {
struct thrd_info *tinfo = arg;
int elt;
atomic_init(&tinfo->alive, true);
/* busy waiting when queue empty */
while (atomic_load(&tinfo->alive)) {
if (thrd_trydeq(&tinfo->queue, &elt)) {
sched_yield();
continue;
}
printf("Thread %zu deq %d\n",
tinfo->thrd_num, elt);
}
pthread_exit(NULL);
}
有asm fences
关于带有 lfence 和 sfence 的特定平台 x86, 如果我删除所有 C11 代码并只用
替换围栏asm volatile ("sfence" ::: "memory");
和
asm volatile ("lfence" ::: "memory");
(我对这些宏的理解是:防止内存访问的编译器栅栏是 reoganized/optimized + 硬件栅栏)
例如,我的变量是否需要声明为 volatile?
我已经看到上面的环形缓冲区代码只有这些 asm fences 但没有原子类型,我真的很惊讶,我想知道这段代码是否正确。
我只是回复关于 C11 原子,平台细节太复杂,应该逐步淘汰。
C11 中线程之间的同步只能通过一些系统调用(例如 mtx_t
)和原子来保证。甚至不要尝试没有它。
也就是说,同步化 通过 原子进行,也就是说,副作用的可见性保证通过对原子的影响的可见性传播。例如,对于最简单的一致性模型,顺序模型,每当线程 T2 看到线程 T1 对原子变量 A 产生影响的修改时,线程 T1 中该修改之前的所有副作用对 T2 都是可见的。
所以并不是所有的共享变量都需要是原子的,你只需要确保你的状态通过原子正确传播。从这个意义上说,当您使用顺序或 acquire-release 一致性时,栅栏不会给您带来任何好处,它们只会使图片复杂化。
一些更一般的评论:
- 由于您似乎使用了顺序一致性模型,因此
默认情况下,原子操作的函数式写法(例如
atomic_load
) 是多余的。只是评估原子变量是 一模一样 - 我的印象是你也在尝试优化 在你发展的早期。我认为你应该做一个实现 首先,您可以证明其正确性。那么,当且仅当 你注意到性能问题,你应该开始考虑 优化。这种原子数据结构不太可能 是您应用程序的真正瓶颈。你必须有一个非常 大量的线程同时打击你的穷人 小原子变量,在这里看到一个可测量的瓶颈。