什么是无锁多线程编程

What is lock-free multithreaded programming?

本文关键字:多线程 编程 什么      更新时间:2023-10-16

我看到有人/文章/SO帖子说他们已经为多线程使用设计了自己的"无锁"容器。假设他们没有使用影响性能的模数技巧(即每个线程只能基于一些模数插入),数据结构怎么可能是多线程的,但也是无锁的?

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

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

实际上,甚至锁本身也必须使用那些原子操作!

但是有锁编程和无锁编程之间的区别是,无锁程序永远不会被任何单个线程完全停止。相反,如果在一个锁定程序中,一个线程获得了一个锁,然后被无限期挂起,那么整个程序就会被阻塞,无法继续进行。相比之下,即使单个线程被无限期挂起,无锁程序也可以取得进展。 这里有一个简单的例子:并发计数器自增。我们提供了两个版本,它们都是"线程安全"的,也就是说,它们可以被并发地多次调用。第一个锁定版本:
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_*函数。在锁定的版本中,在持有锁的线程解锁互斥锁之前,没有线程可以进行进程。相比之下,在无锁版本中,所有线程都可以进行进程。如果一个线程被占用了,它就不能完成自己的工作,而其他线程可以继续他们的工作。

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

对于锁,其思想是您获得一个锁,然后在知道没有其他人可以干扰的情况下进行工作,然后释放锁。

对于"无锁",这个想法是你在其他地方做你的工作,然后尝试自动地将这个工作提交到"可见状态",如果失败了再重试。

"无锁"的问题是:

  • 很难为一些重要的事情设计一个无锁的算法。这是因为只有这么多方法来完成"原子提交"部分(通常依赖于原子的"比较和交换",用不同的指针替换指针)。
  • 如果存在争用,它的性能比锁更差,因为您正在重复执行被丢弃/重试的工作
  • 几乎不可能设计出既正确又"公平"的无锁算法。这意味着(在争用下)一些任务可能很幸运(并且反复提交他们的工作并取得进展),而一些任务可能非常不幸(并且反复失败和重试)。

这些东西的组合意味着它只适用于低争用下相对简单的东西。

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

还要注意,有一种叫做"无块"的东西,它就像无锁一样,除了你知道你总是可以提交工作,永远不需要重试。设计一个无块算法更加困难,但争用并不重要,所以无锁的另外两个问题就消失了。注意:Kerrek SB的答案中的"并发计数器"示例根本不是无锁的,但实际上是无块的

"无锁"的概念并不是真的没有任何锁,而是通过使用一些允许我们在大多数操作中不使用锁的技术来最小化锁和/或临界区的数量。

可以使用乐观设计或事务性内存来实现,在这种情况下,您不锁定所有操作的数据,而只锁定某些特定点(当在事务性内存中执行事务时,或者在乐观设计中需要回滚时)。

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

然而,要实现其中一个的底层机制-一些锁定将最有可能使用,但数据被锁定的时间(假设)保持在最小,如果这些技术被正确使用。

新的C和c++标准(C11和c++ 11)引入了线程,以及线程共享的原子数据类型和操作。原子操作为两个线程之间发生竞争的操作提供了保证。一旦线程从这样的操作返回,它就可以确定操作已经完整地完成了。

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

除了是原子数据类型之外,数据类型还可以具有"无锁"财产。这也许应该被称为"无状态",因为这个属性意味着对这种类型的操作永远不会使对象处于中间状态,即使它被中断处理程序中断,或者另一个线程的读取处于更新过程中。

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