有效的C ++条件自旋锁是否可行?

时间:2018-12-03 01:29:22

标签: c++ multithreading

我在多线程C ++代码中遇到一种情况,我需要使一些非常快速的操作原子化(显示为序列化),因此我可以使用自旋锁,例如:

lock mutex: while (lock.test_and_set(std::memory_order_acquire))
unlock mutex: lock.clear(std::memory_order_release);

但是我认为很聪明,并以当前是否由多个线程共享数据结构为条件来锁定:

lock mutex: if(lockneeded) while (lock.test_and_set(std::memory_order_acquire))
unlock mutex: if(lockneeded)lock.clear(std::memory_order_release);

最初,数据结构仅由一个线程拥有,但所有者可以授予对另一个线程的访问权限,此时必须设置需要锁定的变量(该变量必须为原子布尔本身)。

这项工作有效吗?


编辑:一些上下文。我有一个安排协程的系统。由一个线程一次运行一个挂起的协程队列,直到它挂起或完成,然后再运行下一个。该系统最初是为单线程设计的,因为规范中的协程是顺序编程结构。上下文切换时间非常快,因为协程将堆栈分配的链表用于堆栈,而不是机器堆栈。因此,上下文切换基本上只是指针交换。

然后,我决定可以选择允许多个线程来处理列表,因此协程成为进程。现在,指针交换必须原子完成。交换非常快,因此自旋锁似乎是保护操作的正确方法。

我有一个测试用例,其中我依次运行一组作业,然后使用额外的帮助程序线程再次执行。我有一个现已解决的问题,事实证明它与计划无关。现在,有4个线程运行该进程的速度大约是1的3.5倍。

性能目标很简单:我想抹去Go-lang的痕迹。我的系统兼容C / C ++ ABI(不支持Go),它使用正确的模型进行流处理(不支持Go),并且它还具有非常出色的语言。

我不知道Go可以快速切换上下文。但是我的测试用例的当前未调整版本(在此版本中,我们不能忘记作业计数为100K来创建延迟(并确保锁上的争用几乎为零))正在5秒钟内处理200万个进程,这是上下文切换率每秒约40万个开关。我希望如果我用空作业代替慢作业(不做任何协程),则速率将超过每秒1亿个开关。正在运行200万个进程。现实世界中的速度会更低,实验正在尝试找出性能的上限。

1 个答案:

答案 0 :(得分:1)

不,不幸的是,这行不通。

说线程A看到lockneeded为假,并进入临界区而未获取lock,然后在临界区的中间发生上下文切换。线程B请求访问数据结构。数据结构不知道线程A在关键部分,因此授予了线程B访问权限。 lockneeded设置为true,但是线程A已经在其关键部分内。然后线程B获得lock ...您可以轻松地看到这是未定义的行为。

除非您可以保证在关键部分lockneeded不会发生变化,否则它将无法正常工作。确保lockneeded不变的一种方法是使用锁来保护它。因此,您需要为lockneeded的每次访问添加一个锁,从而首先破坏了变量的用途。

高效的C ++自旋锁

自旋锁在概念上是如此简单,但是有很多可用的口味。要考虑的重要因素是性能要求(真的真的需要它们吗?),体系结构,线程库,所需的可伸缩性,预期的争用量(如果争用很少,则可以针对非争用情况),使用相同锁定的关键部分的不对称性(以防止线程出现饥饿),读写比率……您可以看到,如果您需要超级高效,就需要进行大量的性能测试去做。因此,如果您真的不需要性能,则应该使用已有的自旋锁,然后将时间花在其他地方。

但是我们是计算机科学家,我们喜欢最有效的解决方案,因为我们是问题解决者。要获得高度争议的,高度可伸缩的自旋锁,请查看MCS锁。对于总体上良好的自旋锁,我前段时间进行了一些测试,发现pthreads的自旋锁具有很好的可扩展性。

还有另一种方法可以确保线程A不在关键部分,而无需线程A编写任何内容。称为rcu_synchronize,并且要过度简化,它涉及线程B设置lockneeded,并等待足够的时间以确保关键部分中的任何线程都能完成它。

由于锁变量的高速缓存未命中(由于全局写操作使其他也在旋转的内核无效),因此总线流量导致天真自旋锁的伸缩性很差。

您可以执行的一个简单优化是“读取时旋转”自旋锁:

lock mutex:   while (lock.load(std::memory_order_acquire) || lock.test_and_set(std::memory_order_acquire)) {}
unlock mutex: no change

因此,如果另一个线程拥有该锁,则该线程不会受到TSL的困扰(由于OR短路),但是当另一个线程释放该锁时,该线程会尝试TSL,这可能成功也可能不会成功。不幸的是,这种锁定在大规模情况下的性能与幼稚的自旋锁一样差,但是在低规模,中等竞争的情况下,与幼稚的自旋锁相比,有时会为您节省一些时间。