什么是无锁多线程编程?

时间:2012-12-23 14:42:12

标签: c++ c multithreading algorithm locking

我见过人/文章/ SO帖子,他们说他们设计了自己的“无锁”容器,用于多线程使用。假设他们没有使用性能命中模数技巧(即每个线程只能基于某些模数插入)数据结构如何是多线程的,但也是无锁的?

这个问题是针对C和C ++的。

5 个答案:

答案 0 :(得分:31)

无锁编程的关键是使用硬件内在的原子操作。

事实上,即使锁本身也必须使用那些原子操作!

但是,锁定和无锁编程之间的区别在于,无锁程序永远不会被任何单个线程完全停止。相比之下,如果在一个锁定程序中,一个线程获得一个锁,然后无限期地被挂起,整个程序就被阻止,无法取得进展。相比之下,即使单个线程被无限期暂停,无锁程序也可以取得进展。

这是一个简单的例子:并发计数器增量。我们提出两个版本都是“线程安全的”,即可以同时多次调用。首先是锁定版本:

int counter = 0;
std::mutex counter_mutex;

void increment_with_lock()
{
    std::lock_guard<std::mutex> _(counter_mutex);
    ++counter;
}

现在是无锁版本:

std::atomic<int> counter(0);

void increment_lockfree()
{
    ++counter;
}

现在假设数百个线程同时调用increment_*函数。在锁定版本中,没有线程可以进行,直到锁定线程解锁互斥锁。相比之下,在无锁版本中,所有线程都可以取得进展。如果一个线程被搁置,它就不会分担工作,但其他人都可以继续工作。

值得注意的是,通常无锁编程可以交换吞吐量和平均延迟吞吐量,以实现可预测的延迟。也就是说,如果没有太多的争用(由于原子操作很慢并影响系统的其他部分),无锁程序通常比相应的锁定程序更少完成,但它保证永远不会产生不可预测的大的延迟。

答案 1 :(得分:16)

对于锁定,我们的想法是你获得一个锁,然后知道没有其他人可以干涉你的工作,然后释放锁。

对于“无锁”,我的想法是你在其他地方做你的工作,然后尝试将这项工作原子地提交到“可见状态”,如果你失败则重试。

“无锁”的问题是:

  • 很难为一些不重要的东西设计一个无锁算法。这是因为只有很多方法可以执行“原子提交”部分(通常依赖于原子“比较和交换”,用不同的指针替换指针)。
  • 如果有争用,它会比锁更糟糕,因为你反复做的工作被丢弃/重试
  • 设计一个既正确又“公平”的无锁算法几乎是不可能的。这意味着(在争论中)某些任务可能是幸运的(并且反复提交他们的工作并取得进展),而有些任务可能非常不幸(并且反复失败并重试)。

这些事情的结合意味着它只对低争用的相对简单的事物有好处。

研究人员设计了无锁链接列表(和FIFO / FILO队列)和一些无锁树。我认为没有比这更复杂的东西了。对于这些东西是如何工作的,因为它很难复杂。最理智的方法是确定您感兴趣的数据结构类型,然后在网上搜索该数据结构的无锁算法的相关研究。

另请注意,有一种称为“无块”的东西,它就像无锁,除了你知道你总是可以提交工作而不需要重试。设计一个无块算法更难,但争用并不重要,因此其他两个无锁问题就会消失。 注意:Kerrek SB答案中的“并发计数器”示例根本不是锁定的,但实际上是无块的。

答案 2 :(得分:4)

“免费锁定”的想法实际上并没有任何锁定,我们的想法是通过使用一些允许我们不在大多数操作中使用锁的技术来最小化锁和/或关键部分的数量。

可以使用optimistic designtransactional memory来实现,您不会锁定所有操作的数据,而只能锁定某些特定点(在事务内存中执行事务时,或者需要时)在乐观设计中回滚。)

其他替代方案基于某些命令的原子实现,例如CAS(比较和交换),甚至允许我们在给定其实现的情况下解决consensus problem。通过对引用进行交换(并且没有线程正在处理公共数据),CAS机制允许我们轻松实现无锁的乐观设计(当且仅当没有人已经更改它时交换到新数据,并且这是原子地完成的。)

但是,要实现其中之一的基础机制 - 某些锁定最有可能,但数据被锁定的时间(假设)保持最小,如果正确使用这些技术。

答案 3 :(得分:4)

新的C和C ++标准(C11和C ++ 11)具有线程并在线程之间共享原子数据类型和操作。原子操作为在两个线程之间进行竞争的操作提供保证。一旦线程从这样的操作返回,就可以确保操作完全受到影响。

这种原子操作的典型处理器支持存在于现代处理器上,用于比较和交换(CAS)或原子增量。

除了原子之外,数据类型还可以具有“无锁”属性。这或许应该被创造为“无状态”,因为这个属性暗示对这种类型的操作永远不会使对象处于中间状态,即使它被中断处理程序中断或者另一个线程的读取落在中间更新。

几种原子类型可能(或可能不)是无锁的,有一些宏可以测试该属性。始终有一种类型可以保证无锁,即atomic_flag。 `

答案 4 :(得分:0)

你在Windows中拥有互锁库 http://msdn.microsoft.com/en-us/library/windows/desktop/ms683590(v=vs.85).aspx 这被认为无锁。

* il只引用msdn作为我不是大专家: 但基本上如果硬件前提它会为你提供一个处理这个“lockfree”的原子操作,否则它将提供一个锁定它使用内存barries技术 *

这里有_InterlockedExchange的几个变体,它们根据它们涉及的数据类型以及是否使用特定于处理器的获取或释放语义而变化。 当_InterlockedExchange函数对32位整数值进行操作时,_InterlockedExchange64对64位整数值进行操作。 _InterlockedExchange_acq和_InterlockedExchange64_acq内部函数与没有_acq后缀的相应函数相同,除了使用acquire语义执行操作,这在进入临界区时很有用。 此函数的任何版本都不使用发布语义。 在Visual C ++ 2005中,这些函数表现为读写内存屏障。有关更多信息,请参阅_ReadWriteBarrier。 这些例程仅作为内在函数提供。