2017-02-04 75 views
0

我只想让我的代码尽可能简单并且线程安全。SPSC螺纹安全栅栏

随着C11原子学

关于部分ISO/IEC 9899/201X草案

X和Y的 “7.17.4栅栏”,在某些原子物体M两种操作,使得A是 在X之前测序,X修改M,Y在B之前测序,且Y读取由X写入的值或由 假设版本序列X中的任何副作用写入的值如果它是版本 操作。

此代码是否线程安全(将“w_i”作为“对象M”)?
“w_i”和“r_i”是否都需要声明为_Atomic?
如果只有w_i是_Atomic,那么主线程是否可以在缓存中保留r_i的旧值并将队列视为未满(填满时)并写入数据?
如果我读了一个没有atomic_load的原子,怎么回事?

我做了一些测试,但所有的尝试似乎都给出了正确的结果。但是,我知道我的测试对于多线程来说并不是真的正确:我多次运行程序并查看结果。

即使既不将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围栏

关于与lfence和SFENCE, 特定平台的x86如果我删除所有的C11代码,只是通过

asm volatile ("sfence" ::: "memory"); 

asm volatile ("lfence" ::: "memory"); 

代替栅栏(我的这些宏观的理解是:编译篱笆以防止内存访问被reoganized /优化+硬件围栏)

我的变量是否需要声明为volatile?

我已经看过上面这个环形缓冲区代码,只有这些asm的栅栏,但没有原子类型,我真的很惊讶,我想知道这段代码是否正确。

回答

1

我只是回复关于C11原子,平台的细节太复杂,应该逐步淘汰。

只有通过一些系统调用(例如mtx_t)和原子才能保证C11中线程之间的同步。甚至不要尝试没有。

也就是说,同步工作通过原子,即副作用的可见性保证通过影响原子的可见性传播。例如,对于最简单的一致性模型,顺序的,只要线程T2看到修改线程T1对原子变量A产生影响,线程T1中修改之前的所有副作用对于T2都是可见的。

因此,并非所有共享变量都必须是原子的,您只需确保您的状态通过原子正确传播。在这种意义上,当你使用顺序或获取释放一致性时,栅栏不会为你购买任何东西,它们只会使图片复杂化。

一些更普遍的言论:

  • 因为你似乎使用顺序一致性模型,这是 默认情况下,原子操作的功能性写作(例如 atomic_load)是多余的。只是评估原子变量是 完全一样。
  • 我的印象是,在您的开发早期,您正尝试优化太多 。我认为你应该先执行 ,你可以证明它是正确的。然后,当且仅当 您注意到性能问题,您应该开始考虑优化 。这种原子数据结构很可能不会成为您应用的真正瓶颈。你必须拥有大量的线程,这些线程全部同时对你的可怜的原子变量进行锤击,以在这里看到可测量的瓶颈。
+0

谢谢!如果我避免使用围墙,我必须使用atomic_store_explicit和所需的内存顺序?或者我只写:queue-> w_i = next_w_i(这是默认使用的放松顺序?) 在哪种情况下我应该使用fence? – treywelsh

+0

不,默认顺序是顺序一致性,最强可能。 –