带栅栏的 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) 是多余的。只是评估原子变量是 一模一样
  • 我的印象是你也在尝试优化 在你发展的早期。我认为你应该做一个实现 首先,您可以证明其正确性。那么,当且仅当 你注意到性能问题,你应该开始考虑 优化。这种原子数据结构不太可能 是您应用程序的真正瓶颈。你必须有一个非常 大量的线程同时打击你的穷人 小原子变量,在这里看到一个可测量的瓶颈。