无锁C ++数据结构,不可能?

时间:2014-03-01 20:50:26

标签: c++ multithreading concurrency lockless

我真的不明白你如何使一些数据结构无锁。例如,如果您有一个链接列表,那么要么用互斥体包围操作,要么在忙于重新链接新节点时执行另一个线程,最终会出现竞争条件。

“免费锁定”的概念(我很欣赏它并不意味着“没有锁定”,但意味着线程可以在不等待其他线程完成的情况下继续进行)只是没有意义。

有人可以给我一个使用堆栈,队列或链表等的简单示例,它实现为“无锁”,因为我无法理解如何在不干扰其他线程生产力的情况下防止竞争条件?当然这两个目标相互矛盾?

5 个答案:

答案 0 :(得分:4)

无锁数据结构使用原子操作,可能会产生额外的要求。例如,数据结构可能仅对一个读取器和一个写入器线程或任何其他组合是安全的。在简单链接列表的情况下,将使用原子读取和写入节点指针,以保证多个线程可以安全地同时读取和写入它。

你可能会或可能不会侥幸逃脱。如果您需要有关数据结构和验证内容的额外保证,那么如果没有某种形式的高级锁定,您可能无法做到这一点。此外,即使考虑到有关如何使用数据结构的其他要求,也不是每个数据结构都允许重写为无锁。在这种情况下,不可变对象可能是一个解决方案,但是由于复制它们通常会带来性能损失,这并不总是比锁定对象然后改变它更理想。

答案 1 :(得分:2)

有许多不同的原语可以让人们构建这种无锁数据结构。例如,比较和交换(简称CAS)以原子方式执行以下代码:

CAS(x, o, n)
  if x == o:
    x = n
    return o
  else:
    return x

通过此操作,您可以执行原子更新。例如,考虑一个非常简单的链表,它以排序顺序存储元素,允许您插入新元素并检查元素是否已存在。 find操作将像以前一样工作:它将遍历所有链接,直到它找到一个元素,或找到比查询更大的元素。插入需要更加小心。它可以如下工作:

insert(lst, x)
  xn = new-node(x)
  n = lst.head
  while True:
    n = find-before(n, x)
    xn.next = next = n.next
    if CAS(n.next, next, x) == next:
      break

find-before(n,x)只是在订单中找到x之前的元素。当然,这只是一个草图。一旦你想支持删除,事情会变得更复杂。我推荐Herlihy和Shavit的“多处理器编程艺术”。我还应该指出,切换实现相同模型的数据结构通常是有利的,以使它们无锁。例如,如果要实现等效的std::map,使用红黑树进行操作会很麻烦,但跳过列表更易于管理。

答案 2 :(得分:1)

无锁结构使用原子指令获取资源的所有权。原子指令锁定它在CPU缓存级别工作的变量,确保另一个核心不会干扰操作。

假设你有这些原子指令:

  • 阅读(A) - > A
  • compare_and_swap(A,B,C) - > oldA = A; if(A == B){A = C};返回oldA;

使用这些指令,您只需创建一个堆栈:

template<typename T, size_t SIZE>
struct LocklessStack
{
public:
  LocklessStack() : top(0)
  {
  }
  void push(const T& a)
  {
     int slot;
     do
     {
       do
       {
         slot = read(top);
         if (slot == SIZE)
         {
           throw StackOverflow();
         }
       }while(compare_and_swap(top, slot, slot+1) == slot);
       // NOTE: If this thread stop here. Another thread pop and push
       //       a value, this thread will overwrite that value [ABA Problem].
       //       This solution is for illustrative porpoise only
       data[slot] = a;
     }while( compare_and_swap(top, slot, slot+1) == slot );
  }
  T pop()
  {
     int slot;
     T temp;
     do
     {
       slot = read(top);
       if (slot == 0)
       {
         throw StackUnderflow();
       }
       temp = data[slot-1];
     }while(compare_and_swap(top, slot, slot-1) == slot);
     return temp;
  }
private:
  volatile int top;
  T data[SIZE];
};

volatile是必需的,因此编译器不会在优化期间弄乱操作顺序。 发生两次并发推送:

第一个进入while循环并读取插槽,然后第二个推入到达,读取顶部,比较和交换(CAS)成功并增加顶部。另一个线程唤醒,CAS失败并再次读取顶部..

发生两次并发弹出:

与之前的案例非常相似。需要阅读该值。

同时发生一次弹出和推动:

pop读取顶部,读取temp ..按Enter键并修改top并推送一个新值。 Pop CAS失败,pop()将再次执行循环并读取新值

按下顶部并获取一个插槽,弹出输入并修改顶部值。推送CAS失败,必须再次循环推动较低的索引。

显然在并发环境中并非如此

stack.push(A);
B = stack.pop();
assert(A == B); // may fail

因为push是原子而pop是原子的,它们的组合不是原子的。

Game programming gem 6的第一章是一个很好的参考。

请注意,代码未经过测试,原子可以是really nasty

答案 3 :(得分:0)

你对锁定自由的定义是错误的。

  

锁定自由允许单个线程饿死但保证系统范围的吞吐量。如果算法满足当程序线程运行足够长时至少有一个线程进展(对于某些明确的进度定义),则该算法是无锁的   https://en.wikipedia.org/wiki/Non-blocking_algorithm

这意味着,多个线程访问数据结构只会被授予1;其余的将失败

锁定自由的重要一点是内存冲突的可能性。 使用锁保护的数据结构通常比具有原子变量的实现更快,但它不能很好地扩展,并且碰撞的可能性很小。

示例:多个线程不断推送列表中的数据。这将导致许多碰撞和经典的互斥体都很好。但是,如果您有1个线程将数据推送到列表的末尾,并且1个线程在前面弹出数据,则情况会有所不同。如果列表不为空,则push_back()和pop_front()不会发生冲突(取决于实现),因为它们不对同一个对象起作用。但是仍然会更改空列表,因此您仍需要保护访问权限。在这种情况下,锁定自由将是更好的解决方案,因为您可以同时调用这两个函数而无需等待。

简而言之:无锁是专为大型数据结构而设计的,其中多个编写器大多是分离的,很少发生冲突。

我试着在我自己实现一个无锁列表容器... https://codereview.stackexchange.com/questions/123201/lock-free-list-in-c

答案 4 :(得分:-1)

假设一个简单的操作,将变量递增1。如果你使用&#34实现这个;从内存中读取变量到cpu,在cpu寄存器中加1,把变量写回#34;然后你必须在整个事情周围放一些互斥量,因为你想要确保第二个线程不会读取变量,直到之后第一个线程将其写回来。

如果您的处理器具有原子&#34;递增内存位置&#34;装配说明,你不需要锁。

或者,假设您要将元素插入到链接列表中,这意味着您需要使开始指针指向新元素,然后使新元素指向前一个元素。用原子&#34;交换两个存储单元&#34;操作,您可以将当前的开始指针写入&#34; next&#34;新元素的指针,然后交换两个指针 - 现在,根据首先运行的线程,元素在列表中的顺序不同,但列表数据结构保持不变。

基本上,它总是在一次原子操作中同时做几件事,所以你不能将操作分解成可能不会被打断的单个部分。&#34;。 / p>