无锁队列实现最终会在压力下产生循环

时间:2010-04-26 17:28:21

标签: c multithreading queue lockless

我有一个用C语言编写的无锁队列,它是一个链表,包含发布到单个线程并在单个线程中处理的多个线程的请求。经过几个小时的压力后,我最终得到了最后一个请求的下一个指针指向自身,这会创建一个无限循环并锁定处理线程。

应用程序在Linux和Windows上运行(并失败)。我在Windows上进行调试,我的COMPARE_EXCHANGE_PTR映射到InterlockedCompareExchangePointer

这是将请求推送到列表头部的代码,并从多个线程调用:

void push_request(struct request * volatile * root, struct request * request)
{
    assert(request);

    do {
        request->next = *root;
    } while(COMPARE_EXCHANGE_PTR(root, request, request->next) != request->next);
}

这是从列表末尾获取请求的代码,仅由处理它们的单个线程调用:

struct request * pop_request(struct request * volatile * root)
{
    struct request * volatile * p;
    struct request * request;

    do {
        p = root;
        while(*p && (*p)->next) p = &(*p)->next; // <- loops here
        request = *p;
    } while(COMPARE_EXCHANGE_PTR(p, NULL, request) != request);

     assert(request->next == NULL);

     return request;
}

请注意,我没有使用尾指针,因为我想避免在push_request中处理尾指针的复杂性。但是我怀疑问题可能在于我找到列表末尾的方式。

有几个地方将请求推送到队列中,但它们看起来都像这样:

// device->requests is defined as struct request * volatile requests;
struct request * request = malloc(sizeof(struct request));
if(request) {
    // fill out request fields
    push_request(&device->requests, request);
    sem_post(device->request_sem);
}

处理请求的代码正在做更多的事情,但实质上是在循环中执行此操作:

if(sem_wait_timeout(device->request_sem, timeout) == sem_success) {
    struct request * request = pop_request(&device->requests);
    // handle request
    free(request);
}

我还添加了一个函数,它在每个操作之前和之后检查列表中的重复项,但我担心这个检查会改变时间,以便我永远不会遇到失败的地方。 (我正在等待它打破,因为我正在写这个。)

当我中断挂起程序时,处理程序线程在pop_request处在标记位置循环。我有一个或多个请求的有效列表,最后一个指针指向自己。请求队列通常很短,我从未见过超过10个,只有1和3两次我可以在调试器中查看这个失败。

我尽可能地想到了这一点,我得出的结论是,除非我两次推送相同的请求,否则我永远不能在我的列表中找到一个循环。我很确定这种情况永远不会发生。我也很确定(虽然不完全)它不是ABA problem

我知道我可能会同时发出多个请求,但我相信这与此无关,而且我从未见过这种情况。 (我也会解决这个问题)

我对如何破坏我的功能感到很久很难,但我没有办法找到一种方法来结束循环。

所以问题是:有人能看到这样的方式吗?有人可以证明这不可以吗?

最终我会解决这个问题(也许是通过使用尾指针或其他一些解决方案 - 锁定将是一个问题,因为发布的线程不应该被锁定,虽然我手边有一个RW锁)但是我想确保更改列表实际上解决了我的问题(而不是因为时间不同而使它更不可能)。

1 个答案:

答案 0 :(得分:8)

这很微妙,但那里确实存在竞争条件。

从包含一个元素的列表req1开始。所以我们有:

device->requests == req1;
req1->next == NULL;

现在,我们推送一个新元素req2,同时尝试弹出队列。推送req2首先开始。 while循环体运行,所以我们现在有:

device->requests == req1;
req1->next == NULL;
req2->next == req1;

然后COMPARE_EXCHANGE_PTR运行,所以我们有:

device->requests == req2;
req1->next == NULL;
req2->next == req1;

... COMPARE_EXCHANGE_PTR()返回req1。现在,此时,在<{1}}条件中进行比较之前,推送会中断并且流行开始运行。

pop正常运行完成,弹出while - 这意味着我们有:

req1

推送重启。它现在提取device->requests == req2; req2->next == NULL; 来进行比较 - 它会获取request->next值,即req2->next。它将NULLreq1进行比较,比较成功,while循环再次运行,现在我们有:

NULL

这次测试失败,while循环退出,你有循环。


这应该解决它:

device->requests == req2;
req2->next == req2;