我正在使用" 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_first
和malloc
只有一次CAS操作。
问题:由于无锁算法很难,我不确定我是否正确。我是否忽略了可能导致竞争状况的事情?
答案 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发明的)。