无锁简单隔离存储算法

时间:2014-10-31 00:24:05

标签: c++ c++11 atomic lock-free

我正在使用" Simple Segregated Storage"的无锁版本。 C ++中的内存池。

SSS内存池类似于slab分配器:它基本上只是一块内存,被分成大小相等的块,我们有一个指向第一个可用块的空闲列表指针。分配只是将指针移动到下一个块,而解除分配只是将空闲列表指针设置为解除分配的块,然后指向" next"解除分配的块上的指针指向空闲列表指针的旧值。

所以它基本上是一个单链表。

现在,我正在尝试编写Simple Segregated Storage算法的无锁版本。如果我们假设SEGREGATING最初的内存块(即创建链表)总是在进入多线程环境之前完成,我们只需要担心分配和释放块 - 在这种情况下这个问题变得非常类似于无锁单链表,这是一个很好理解的问题。

因此,在我看来,通过使用简单的比较和交换指令,可以轻松地以无锁方式完成分配和解除分配。

假设我们有以下空闲列表指针:

std::atomic<unsigned char*> m_first

并假设我们有一个辅助函数nextof(),它接受​​一个块指针并返回对&#34; next指针&#34;的引用。下一个指针实际上嵌入在内存块中 - 所以nextof函数看起来像这样:

unsigned char*& nextof(void* ptr)
{
    return *static_cast<unsigned char**>(ptr);
}

这至少是Boost library implements Simple Segregated Storage

的方式

无论如何,我对无锁,原子allocate函数的尝试是:

void* malloc()
{
    unsigned char* first = m_first.load();
    if (!first) return nullptr;

    while (!m_first.compare_exchange_strong(first, nextof(first))) 
    {
        if (!first) break;
    }

    return first;
}

所以 - 这看起来非常简单。我们需要做的就是将m_first原子地设置为嵌入m_first指向的块中的下一个指针。最初,我认为这就像说m_first.store(nextof(m_first))一样简单 - 但不幸的是我不认为这是原子的,因为我们实际上是在同一个内存上做存储和加载location - 我们会将新值存储到m_first中,但也会加载m_first的值,以便获取它的下一个指针。

所以,似乎我们需要进行比较和交换。在上面的代码中,我原子地将m_first的值加载到局部变量中。然后,在检查null之后,我原则上使用CAS更改m_first的值,当且仅当它仍然等于局部变量的值时。如果它没有,则局部变量将更新为现在的m_first,并且我们将继续循环,直到我们没有被其他线程抢占。

重新分配有点棘手 - 在这里我们需要将m_first的值更改为解除分配的块,并且更新解除分配的块上的下一个指针以指向{曾经是{1}}。

我的尝试是:

m_first

我不太确定我有这个权利,但我认为这是对的。首先,我原子地加载void free(void* chunk) { unsigned char* first = m_first.load(); nextof(chunk) = first; while (!m_first.compare_exchange_strong(first, static_cast<unsigned char*>(chunk))) { nextof(chunk) = first; } } 的值并将其存储在局部变量中。然后,我将下一个指针放在要发布的m_first的本地副本上。当然,到现在为止,另一个线程可能已经介入并将m_first设置为其他东西 - 所以我现在必须执行CAS操作来检查m_first是否仍然是我所期望的。如果不是,我必须将下一个指针设置为适当的值,然后继续循环。

据我所知,这是所有竞争条件都安全的,m_firstmalloc只有一次CAS操作。

问题:由于无锁算法很难,我不确定我是否正确。我是否忽略了可能导致竞争状况的事情?

1 个答案:

答案 0 :(得分:0)

旧答案,但我想我可能会发表意见。除非我弄错了,否则我相信代码可能会通过ABA problem的版本泄漏数据。

问题是在将参数nextof(first)传递给compare_exchange_strong内的原子malloc之前对其进行评估。如果我们从while函数中获取malloc循环:

while (!m_first.compare_exchange_strong(first, nextof(first)))
{
    if (!first) return nullptr;
}

并在传递给nextof(first)之前重写它以明确评估compare_exchange_strong,以使其更明显:

while (true)
{
    auto tmp_next = nextof(first);

    // this is where the chance for ABA exists

    if (m_first.compare_exchange_strong(first, tmp_next))
        break;

    if (!first) return nullptr;
}

然后似乎有m_first可能会更改为不同的值并返回这两行之间,而另一个线程同时改变实际的nextof(m_first)

// I will represent the linked list using letters,
// i.e. m_first -> "A" means that "A" is at the head,
// and "A" -> "B" means that "A" points to "B"

void* malloc()
{
    // at the start, m_first points to "A": m_first -> "A" -> "B" -> "C"
    unsigned char* first = m_first.load();

    // --> thread #2 calls malloc() here and acquires "A", and
    //     removes it, so: m_first -> "B" -> "C"

    if (!first) return nullptr;

    while (true)
    {
        unsigned char * tmp_next = nextof(first);
        // tmp_next is "B" at this moment, because 'first' is still "A"            

        // --> some third thread returns some even older object now,
        //     so m_first -> "M" -> "B" -> "C"

        // --> thread #2 now calls free() to return "A", resulting in
        //     my_first -> "A" -> "M" -> "B" -> "C"

        // m_first is now back to "A", but tmp_next points
        // to old "B" instead of "M", and the following line succeeds
        // and "M" is lost forever: m_first -> "B" -> "C"

        if (m_first.compare_exchange_strong(first, tmp_next))
            break;

        if (!first) return nullptr;
    }

    return first;
}

Wikipedia article on ABA包含一些解决这些问题的建议,比如“标记状态引用”(基本上向这些指针添加类似“事务计数器”的内容,以便您可以检测到第二个“A”不同作为第一个)或“中间节点”,我发现它实现起来稍微复杂一些(显然是由John D. Valois in this article发明的)。