我在多线程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倍。
表演目标很简单:我想把围棋从地球上抹去。我的系统是兼容C/C++ABI的(Go不是),它使用正确的流处理模型(Go没有),而且它是一种非常优秀的语言。
我不知道Go上下文切换的速度有多快。但是,我的测试用例的当前未经调整的版本是在5秒内处理200万个进程,这是一个每秒约40万个切换的上下文切换速率,在该版本中,我们决不能忘记作业计数为100K以创建延迟(并确保锁上的争用接近于零)。我预计,如果我用空作业(什么都不做协同作业)代替慢作业,速率将超过每秒1 million开关。它运行着200万个进程。现实世界中的速度会更低,实验试图找到性能的上限。
不,不幸的是,这将不起作用。
假设线程A看到lockneeded
为假并在未获取lock
的情况下进入关键部分,则上下文切换发生在关键部分的中间。线程B请求访问数据结构。数据结构不知道线程A在关键部分,因此线程B被授予访问权限。lockneeded
设置为true,但线程A已经在其关键部分内。线程B随后获取lock
。。。您可以很容易地看到这是未定义的行为。
除非你能保证lockneeded
在关键部分不会改变,否则它是不起作用的。确保lockneeded
不会更改的一种方法是使用锁来保护它。因此,您需要为lockneeded
的每次访问添加一个锁,从而从一开始就破坏了变量的目的。
高效的C++自旋锁
spinlock在概念上很简单,但有很多口味可供选择。需要考虑的重要因素是性能要求(它真的需要那么高效吗?)、体系结构、线程库、所需的可扩展性、预期争用量(如果争用很少,您可以针对非争用情况进行优化)、使用相同锁的关键节的不对称性(以防止线程饥饿)、读写比。。。你可以看到,如果你需要它超高效,你需要做很多性能测试。所以,如果你真的不需要性能,你应该使用现有的spinlock,把时间花在其他地方。
但我们是计算机科学家,我们喜欢最有效的解决方案,因为我们是问题的解决者。对于极具争议性、高度可扩展的自旋锁,请查看MCS锁。对于一个总体上很好的spinlock,我不久前进行了一些测试,发现pthreads的spinlock是非常可扩展的。
还有另一种方法可以保证线程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,这可能会成功,也可能不会成功。不幸的是,在高扩展场景中,该锁的性能与天真的自旋锁一样差,但在低扩展、中等争用的情况下,它可能会不时地为您节省一些周期。