Sou*_*a B 45 c++ synchronization lock-free stdatomic
在 C++ 中,有一种原子类型std::atomic<T>。该原子类型可能是无锁的,也可能不是,具体取决于类型 T 和当前平台。如果某个类型的无锁实现在类型 T 的平台上可用,那么大多数编译器都会提供无锁atomic<T>。在这种情况下,即使我想要非无锁atomic<T>我也无法拥有它。
C++ 标准决定只保留一个,std::atomic<T>而不是一std::atomic<T>加一std::lock_free<T>(部分针对特定类型实现)。这是否意味着“在任何情况下,当后者可用时,使用非无锁原子类型都会比使用无锁原子类型更好”?(主要是在性能方面而不是易用性方面)。
Dav*_*rtz 55
这是否意味着“在任何情况下,当后者可用时,使用非无锁原子类型都会比使用无锁原子类型更好”?(主要是在性能方面而不是易用性方面)。
不。总的来说,这不是真的。
假设您有两个核心和三个可供运行的线程。假设线程 A 和 B 正在访问相同的集合,并且会发生显着的争用,而线程 C 正在访问完全不同的数据,并且会最小化争用。
如果线程 A 和 B 使用锁,其中一个线程将很快被取消调度,而线程 C 将在一个内核上运行。这将允许调度的线程 A 或 B 几乎没有争用地运行。
相比之下,使用无锁集合时,调度程序永远没有机会重新调度线程 A 或 B。线程 A 和 B 完全有可能在其整个时间片中同时运行,从而在其 L2 之间对相同的缓存行进行乒乓操作。全程缓存。
一般来说,锁比无锁代码更有效。这就是为什么在线程代码中更频繁地使用锁的原因。然而,std::atomic类型通常不用于这样的上下文中。std::atomic在您有理由认为锁会更有效的上下文中使用类型可能是错误的。
Jer*_*fin 39
除了大卫·施瓦茨(David Schwartz)的出色回答之外,我要指出的是,很大程度上取决于整个系统中稀缺的东西。
如果准备运行的线程多于运行它们的核心,那么您通常想要做的是尽快检测到对某些资源的争用,并将除其中一个争用线程之外的所有线程置于睡眠状态,这样您就可以可以将其他线程调度到这些核心上。
无锁往往在或多或少相反的情况下效果更好:在任何给定时间,可用的硬件多于要运行的线程。在这种情况下,当资源空闲时,具有无锁代码的忙等待可以非常快速地做出反应,进行修改并继续前进。
第二个问题是当争用确实发生时可能会持续多长时间。如果您有许多线程不断地“争夺”一些资源,那么您几乎肯定最好让大多数线程进入睡眠状态,让少数线程(通常只有一个)尽快取得进展,然后切换到另一个线程并重复。
但是让一个线程进入睡眠状态并调度另一个线程意味着进入内核模式和调度程序。如果争用预期是短暂的,则线程之间的不断切换会增加大量开销,因此整个系统会减慢很多。
Mat*_* M. 26
这是否意味着“在任何情况下,当后者可用时,使用非无锁原子类型都会比使用无锁原子类型更好”?
是的,在这个具体案例中。
无锁实现总是优于锁定实现的原因std::atomic<T>很简单,这些操作是由硬件本身支持的。
也就是说,std::atomic_uint32_t::load(std::memory_order::relaxed)在 x86_64 上将归结为:
mov eax, DWORD PTR [rsp-4]
Run Code Online (Sandbox Code Playgroud)
这只是常规的内存读取,因为 x86 默认情况下已经具有强大的内存模型。
当然,这是无与伦比的。
因此,没有必要同时拥有锁定std::locking<std::uint32_t>和无锁std::lock_free<std::uint32_t>:没有任何情况std::locking<std::uint32_t>会更好,它总是会成为性能陷阱。
然而,不要将此视为无锁算法必然更可取的认可。std::atomic无锁的优势来自于直接映射到硬件指令,这是一个相当特殊的情况。正如 @David Schwartz 和 @Jerry Coffin 所解释的,当涉及更复杂的数据结构和更复杂的算法时——尤其是多指令算法——那么无锁和锁定哪个更好就更加微妙。
sup*_*cat 12
无锁数据结构在没有争用时通常表现得很好,即使存在争用也能正确执行,但争用的存在可能会导致其性能严重下降。如果 100 个线程都尝试同时更新无锁数据结构,则一个线程在第一次尝试时会成功,至少两个线程在第一次和第二次尝试中会成功,并且至少三个线程在三次尝试中会成功,等等。但最糟糕的是 -在这种情况下,所需的更新尝试总数可能超过 5,000 次。相比之下,如果 100 个线程都尝试更新基于锁的数据结构,则将允许其中一个线程立即执行更新,而其他 99 个线程将被阻止,但这 99 个线程中的每一个只有在能够执行更新时才会被唤醒。立即访问数据结构。
基于锁的数据结构的缺点是,持有锁的线程将阻塞所有其他线程,除非或直到它完成所需的操作并释放锁。相比之下,当使用无锁数据结构时,被拦截的竞争线程将为其他线程提供比其他线程更少的障碍。