单一生产者单一消费者队列中的内存障碍

时间:2016-10-26 08:53:52

标签: c multithreading queue atomic

我花了几周时间对内存模型,编译器重新排序,CPU重新排序,内存障碍和无锁编程进行了大量阅读,我认为我现在已经陷入混乱。我编写了一个单一的生产者单个消费者队列,并试图找出我需要内存障碍的地方,以及某些操作需要是原子的。我的单个生产者单个消费者队列如下:

typedef struct queue_node_t {
    int data;
    struct queue_node_t *next;
} queue_node_t;

// Empty Queue looks like this:
// HEAD TAIL
//   |    |
// dummy_node

// Queue: insert at TAIL, remove from HEAD
//    HEAD           TAIL
//     |              |
// dummy_node -> 1 -> 2 -> NULL

typedef struct queue_t {
    queue_node_t *head; // consumer consumes from head
    queue_node_t *tail; // producer adds at tail
} queue_t;

queue_node_t *alloc_node(int data) {
    queue_node_t *new_node = (queue_node_t *)malloc(sizeof(queue_node_t));
    new_node->data = data;
    new_node->next = NULL;
    return new_node;
}

queue_t *create_queue() {
    queue_t *new_queue = (queue_t *)malloc(sizeof(queue_t));
    queue_node_t *dummy_node = alloc_node(0);
    dummy_node->next = NULL;
    new_queue->head = dummy_node;
    new_queue->tail = dummy_node;
    // 1. Do we need any kind of barrier to make sure that if the
    // thread that didn't call this performs a queue operation
    // and happens to run on a different CPU that queue structure
    // is fully observed by it? i.e. the head and tail are properly
    // initialized
    return new_queue;
}

// Enqueue modifies tail
void enqueue(queue_t *the_queue, int data) {
    queue_node_t *new_node = alloc_node(data);
    // insert at tail
    new_node->next = NULL;

    // Let us save off the existing tail
    queue_node_t *old_tail = the_queue->tail;

    // Make the new node the new tail
    the_queue->tail = new_node;

    // 2. Store/Store barrier needed here?

    // Link in the new node last so that a concurrent dequeue doesn't see
    // the node until we're done with it
    // I don't know that this needs to be atomic but it does need to have
    // release semantics so that this isn't visible until prior writes are done
    old_tail->next = the_queue->tail;
    return;
}

// Dequeue modifies head
bool dequeue(queue_t *the_queue, int *item) {
    // 3. Do I need any barrier here to make sure if an enqueue already happened
    // I can observe it? i.e., if an enqueue was called on 
    // an empty queue by thread 0 on CPU0 and dequeue is called
    // by thread 1 on CPU1
    // dequeue the oldest item (FIFO) which will be at the head
    if (the_queue->head->next == NULL) {
        return false;
    }
    *item = the_queue->head->next->data;
    queue_node_t *old_head = the_queue->head;
    the_queue->head = the_queue->head->next;
    free(old_head);
    return true;
}

以下是我的问题,与我上面代码中的评论相对应:

  1. create_queue()我回来之前是否需要某种屏障?我想知道我是否从在CPU0上运行的线程0调用此函数,然后使用在线程1中返回的指针,该指针恰好在CPU1上运行,是否可能线程1看到未完全初始化的queue_t结构?
  2. enqueue()中是否需要屏障以确保新节点未链接到队列中,直到所有新节点的字段都被初始化为止?
  3. dequeue()我需要一个障碍吗?我觉得没有一个是正确的,但如果我想确保看到任何已完成的入队,我可能需要一个。
  4. 更新:我试图用代码中的注释清楚地说明,但是这个队列的HEAD总是指向一个虚拟节点。这是一种常见的技术,使得生产者只需要访问TAIL,而消费者只能访问HEAD。空队列将包含一个虚节点,dequeue()总是返回HEAD之后的节点(如果有的话)。当节点出队时,虚节点前进,前一个“虚拟”被释放。

1 个答案:

答案 0 :(得分:0)

首先,它取决于您的特定硬件架构,操作系统,语言等。

1)。 没有。因为你需要一个额外的障碍来将指针传递给另一个线程

2)。 是的,old_tail->next = the_queue->tail需要在the_queue->tail = new_node

之后执行

3)。 它没有任何效果,因为在障碍之前没有任何东西,但理论上你可能需要old_tail->next = the_queue->tail enqueue()之后的障碍。编译器不会在函数外重新排序,但CPU可能会做类似的事情。 (非常不可能,但不是100%肯定)

OT:既然你已经在做一些微优化,你可以为缓存添加一些填充

typedef struct queue_t {
    queue_node_t *head; // consumer consumes from head
    char cache_pad[64]; // head and tail shouldnt be in the same cache-line(->64 Byte)
    queue_node_t *tail; // producer adds at tail
} queue_t;

如果你有足够的内存浪费,你可以做这样的事情

typedef struct queue_node_t {
    int data;
    struct queue_node_t *next;
    char cache_pad[56]; // sizeof(queue_node_t) == 64; only for 32Bit
} queue_node_t;