【问题标题】:C Lock-Free Queue Memory ManagementC 无锁队列内存管理
【发布时间】:2018-07-18 07:45:56
【问题描述】:

为了提高我的 C 技能,我实现了一个线程安全且无锁的队列。该算法来自 Maurice Herlihy 和 Nir ​​Shavit 所著的《多处理器编程的艺术》一书的第 10.5 章,顺便说一句,这是一本很棒的书。

到目前为止,一切正常,但我需要帮助解决以下问题:

问题

free(first) 行在 lfq_deq() 方法中被注释掉,因为如果队列被多个 dequeuer 使用,它可能会导致段错误。如果线程 T1 和 T2 正在出队并且 T1 释放节点而 T2 仍在使用它,则 T2 将产生段错误。

释放此内存的优雅方法是什么?由于我不重用节点,我应该没有 ABA 问题,对吧?还是您认为重用节点并为 ABA 问题实施已知解决方案更容易?

lfq.h

标题提供了一个简单的主要测试方法。

#pragma once
#include <stdlib.h>

typedef struct Node {
    void* data;
    struct Node* next;
} lfq_node_t;

typedef struct Queue {
    lfq_node_t* head;
    lfq_node_t* tail;
} lfq_t;

lfq_t* lfq_new();
void lfq_free(lfq_t* q);
void lfq_enq(lfq_t* q, void* data);
void* lfq_deq(lfq_t* q);

lfq.c

#include "lfq.h"
#include <pthread.h>
#include <stdio.h>

#define CAS(a, b, c) __sync_bool_compare_and_swap(a, b, c)

lfq_t* lfq_new() {
    lfq_t* q = malloc(sizeof(*q));
    lfq_node_t* sentinel = malloc(sizeof(*sentinel));
    sentinel->data = sentinel->next = NULL;
    q->head = q->tail = sentinel;

    return q;
}

void lfq_free(lfq_t* q) {
    lfq_node_t *next, *node = q->head;
    while (node != NULL) {
        next = node->next;
        free(node);
        node = next;
    }
    free(q);
}

void lfq_enq(lfq_t* q, void* data) {
    lfq_node_t *node, *last, *next;

    node = malloc(sizeof(*node));
    node->data = data;
    node->next = NULL;

    while (1) {
        last = q->tail;
        next = last->next;
        if (last == q->tail) {
            if (next == NULL) {
                if (CAS(&(last->next), next, node)) {
                    CAS(&(q->tail), last, node);
                    return;
                }
            } else {
                CAS(&(q->tail), last, next);
            }
        }
    }
}

void* lfq_deq(lfq_t* q) {
    lfq_node_t *first, *last, *next;
    while (1) {
        first = q->head;
        last = q->tail;
        next = first->next;

        if (first == q->head) {
            if (first == last) {
                if (next == NULL) return NULL;
                CAS(&(q->tail), last, next);
            } else {
                void* data = first->next->data;
                if (CAS(&(q->head), first, next)) {
                    // free(first);
                    return data;
                }
            }
        }
    }
}

main.c

一个简单的测试队列的main方法:

#include "lfq.h"
#include <stdio.h>

int main() {
    int values[] = {1, 2, 3, 4, 5};
    lfq_t* q = lfq_new();
    for (int i = 0; i < 5; ++i) {
        printf("ENQ %i\n", values[i]);
        lfq_enq(q, &values[i]);
    }
    for (int i = 0; i < 5; ++i) printf("DEQ %i\n", *(int*)lfq_deq(q));
    lfq_free(q);
    return 0;
}

【问题讨论】:

  • 循环缓冲区(固定大小的数组)是队列的常用解决方案。 Garbage-collection 通过解决释放问题,实际上使基于节点的链表队列更容易。
  • 顺便说一句,您可能应该使用 C11 stdatomic 而不是 GNU C 的 old (and not recommended anymore) __sync_bool_compare_and_swap builtin 的非原子变量。 first = q-&gt;head; ... stuff ... if (first == q-&gt;head) 可以 CSE 两次读取 q-&gt;head 因为它不是原子的(或 volatile,但认真地只是在你的结构中使用 _Atomic 成员。标准原子获取负载可以解决这个问题. C11 CAS给你内存位置的当前值以及一个bool结果,所以你不必在循环中重新读取它
  • 谢谢!我会重写CAS指令等等。我还实现了一个基于数组的解决方案,但出于好奇,我对这个问题的答案很感兴趣。
  • Related: Lock Free stack implementation idea - currently broken 是关于基于节点的 stack (非常类似于队列),但 BeeOnRope 关于内存回收问题的回答可能也与您有关。

标签: c multithreading memory-management memory-leaks lock-free


【解决方案1】:

这看起来像迈克尔和斯科特的队列。

节点不能被释放,我不记得究竟为什么会随意(显然是因为它们仍然可以被引用——但确切地说是我忘记的地方和方式)。它们只能放置在空闲列表中。

我没有仔细检查您的实现是否正确,但我可以看到没有内存障碍,这意味着实现是错误的。

你需要发现、阅读和理解内存屏障,然后使用它们。

我写了两篇文章,可以帮助您入门。

https://www.liblfds.org/mediawiki/index.php?title=Article:Memory_Barriers_%28part_1%29

https://www.liblfds.org/mediawiki/index.php?title=Article:Memory_Barriers_%28part_2%29

【讨论】:

    【解决方案2】:

    正如Peter Cordes在他的评论中指出的,我刚刚发现了内存回收问题:

    相比之下,内存回收是无锁数据结构设计中最具挑战性的方面之一。无锁算法(也称为非阻塞算法)保证只要某个进程继续采取步骤,最终某个进程就会完成一个操作。为无锁数据结构执行内存回收的主要困难在于,进程可能会在持有指向即将被释放的对象的指针时处于休眠状态。因此,不小心释放对象可能会导致睡眠进程在唤醒时访问释放的内存,从而使程序崩溃或产生细微的错误。由于节点未锁定,因此进程必须协调以让彼此知道哪些节点可以安全回收,哪些节点仍可访问。

    引用自奥地利科技研究所 Trevor Brown 的“Reclaiming Memory for Lock-Free Data Structures: There must be a Better Way”

    可以在here 找到一个很好的答案(与堆栈相关但基本相同)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-11
      • 1970-01-01
      • 2013-04-22
      • 2017-04-21
      • 2011-08-30
      相关资源
      最近更新 更多