什么是非阻塞进展?考虑一个简单的例子,即递增计数器C在多个线程之间共享。一种方法是通过保护递增的步骤C使用互斥锁L(即,acquire(L); old := C ; C := old+1; release(L);)。 如果一个线程P持有L,那么另一个线程Q必须等待P释放L才能Q继续操作C。 也就是说,Q被P.
阻塞。 现在考虑使用以下方法实现递增操作:
do {old := C; } until CAS(&C, old, old+1);
此实现是非阻塞的,因为没有线程会被其他线程的不活动(例如,抢占、页错误,甚至终止)阻塞,无论其他线程在哪里停止。
非阻塞进展有三个已确定的级别
最后,
值得注意的是,这些级别的进展保证要求原始的
非阻塞操作通常用于系统或线程间交互,在这些情况下,让线程等待其他线程的动作会使系统容易受到死锁、活锁和/或长时间延迟的影响。 此类用途的示例包括
•异步信号安全。 这允许异步信号的信号处理程序与被中断的线程共享数据或服务,并保证信号处理程序永远不需要等待被中断的
•
系统中使用非阻塞操作时,剩余的进程永远不必等待
•在通用系统上的软实时应用程序。 在此类系统(例如,媒体播放器)上使用非阻塞操作有助于避免优先级反转并提供抢占容忍度。 也就是说,它消除了以下可能性,即一个活动的
与共享计数器的示例不同,几乎任何涉及多个位置的非阻塞操作都会带来多种通常相互矛盾的考虑因素。 本文探讨了
本文的目标是引导读者了解非阻塞操作的各种问题和特性以及要作出的各种选择; 了解这些选项之间的相互作用; 并培养对最佳路径的认识,以理清这些选择之间的相互依赖性,并快速修剪与首先使用非阻塞操作的主要目标不兼容的选项。
对于非阻塞操作的某些用途,例如
图 1 展示了经典
根据上下文,对Header的读取和 CAS 操作可以是单字宽或
暂且忽略为什么整数与Header中的指针打包在一起(稍后解释),读者可以注意到,任何数量的线程在 Push 和 Pop 操作中的任何位置停止活动,都不会阻止其他线程操作列表。 实际上,由于 Push 和 Pop 不仅是非阻塞的,而且也是
以下是在选择非阻塞操作的特性时要考虑的关键问题。
适当的非阻塞进展级别(例如,
为了使用非阻塞操作实现异步信号安全,只需要信号处理程序的操作是非阻塞的,而主线程的操作可以是阻塞的。 例如,在信号处理程序可能搜索哈希表的情况下,哈希表可能被可能被中断的线程更新,完全非阻塞算法不是必需的,而具有非阻塞搜索操作和阻塞添加和删除操作的算法(如 Steve Heller 等人6 所述)可能就足够了。
如果唯一关注的是信号处理程序和被中断线程之间的交互,那么使用
为了避免软实时应用程序中的优先级反转,可能足以使高优先级操作
因此,在选择要使用的适当的非阻塞进展级别时,您必须考虑到为实现主要要求绝对需要什么(例如,
如果可以并发执行某些操作的线程数受到限制,那么与允许更高并发级别时相比,可以使用更简单的算法来实现强大的进展保证(例如,
有时被忽略的一个问题是读取操作对更新操作的影响。 考虑两个缓冲区实现。 两者都支持 check (如果为空)和 add 操作。 在这两种实现中,操作都是
您可以看到后一种实现是多么有问题,当检查缓冲区是否为空的行为可以阻止项目永远添加到缓冲区时。 因此,在为非阻塞操作选择适当的进展级别时,仅仅要求实现是例如
数据结构的选择是设计非阻塞环境时最重要的决策之一。 这一步经常被忽略,因为
例如,如果
另一个例子是,在顺序代码中可能是好选择的搜索结构(例如,平衡树)与哈希结构相比,可能不是非阻塞系统中的最佳选择。
此外,静态结构(例如使用开放寻址的哈希表)比动态结构(例如使用链式寻址的哈希表)更易于管理。
根据定义,非阻塞操作不等待其他非阻塞操作的动作,也不能期望阻止其他非阻塞操作采取动作。 这对取消引用指向可能被其他操作删除和释放的动态结构的指针的非阻塞操作提出了问题。 如果没有适当的预防措施,当动态块从结构中删除并被另一个操作释放时,非阻塞操作可能即将访问该动态块。 这可能会导致一些有问题的结果,例如,如果该块被释放回操作系统,则可能导致访问冲突,破坏恰好分配和使用已释放块的内存的不同结构,或返回不正确的结果。
以
Paul McKenney13 详细讨论了安全内存回收问题和解决方案。 以下是
•自动 GC(垃圾回收)。 在具有 GC 的系统上,例如 Java 应用程序,内存安全是隐式保证的。 在前面的示例中,只要线程P持有对块A的引用,块A就保证不会被释放。 当然,这提出了 GC 本身是否是非阻塞的问题。
•RCU
•引用计数。 块与计数器相关联,当线程获取和释放对这些块的引用时,计数器会递增和递减。 通常,只有当块的引用计数变为零并且保证不会创建对其的新引用时,才会释放该块。20
•Hazard 指针。 简而言之,在这种方法中,每个可能遍历可能被删除和释放的块的线程都拥有多个 hazard 指针。 在取消引用指针之前,线程将其 hazard 指针之一设置为指针值。 其他可能删除该块的线程保证在没有 hazard 指针指向该块之前不会释放该块。8,16
硬件事务内存可以简化非阻塞安全内存回收。4 然而,正如稍后讨论的,大多数即将到来的主流 HTM 实现都是
安全内存回收的选项按灵活性(和
• 已释放的块将被重用,但永远不会为不同的用途释放。
• 已释放的块可以合并并重用于不同的类型和大小,但不会返回给操作系统。
• 已释放的块可以合并、任意重用或返回给操作系统。
请注意,对于某些算法(例如,经典的 LIFO 列表),如果操作系统支持非故障加载,则内存安全可能不是问题。 在刚刚提到的线程P读取地址A在 Pop 的第 2 行中,在地址A处的块被释放到操作系统之后,具有非故障加载的系统将允许这样的读取。
ABA 问题在乐观并发算法中很常见,包括非阻塞算法。 术语 ABA 是指共享变量的值从A变为B再变回A。 以
经典的 LIFO 算法将整数与Header变量中的指针打包在一起,其设计使得如果列表在 Pop 的第 1 行和第 3 行之间发生更改(假设计数器永远不会溢出),计数器将发生更改。
将整数与易受 ABA 问题影响的变量的主要内容打包在一起是经典的 ABA 问题预防解决方案。2 它在几十年中仍然是唯一的解决方案,但它有其局限性。 它需要宽原子操作,以便为足够大的整数留出空间,使其不会溢出(或至少在一次操作的生命周期内不会溢出,例如示例中的 Pop)。 此解决方案的另一个缺点是,它要求整数子字段无限期地保留其语义。 这可能会使释放包含与
虽然 ABA 问题可能发生在不使用动态内存的算法中,但其解决方案与安全内存回收解决方案交织在一起。 首先,如前所述,经典的 ABA 解决方案可能会阻碍内存回收。 其次,任何
RCU 类型解决方案的一个优点是,遍历操作几乎没有开销,而引用计数和 hazard 指针对于遍历操作具有相当大的开销。 另一方面,与 RCU 不同,引用计数和
引用计数的一个缺点在某些情况下可能很显着,那就是它可能导致一个本应是
操作 LL
支持 LL/SC 的实际架构(例如,IBM Power PC)不支持理想的 LL/SC/VL 的完整语义。 没有一个支持 VL 操作,并且所有架构都对 LL/SC 之间可以执行的操作施加限制,并禁止在不同位置嵌套或交错 LL/SC 对。 因此,虽然实际的 LL/SC 支持可以帮助
虽然理想的 LL/SC/VL 的实现提供了一个绝对的 ABA 问题预防解决方案18,但其强大的语义不允许许多正确的场景。 例如,
建议一起考虑 ABA 问题预防和安全内存回收解决方案,以避免不必要的开销重复或与总体系统要求相矛盾或相反的解决方案。
非阻塞算法和方法所需的原子操作的硬件支持范围差异很大。 如果可移植性是一个重要问题,设计人员需要在选择数据结构、算法以及安全内存回收和管理以及 ABA 问题预防的支持方法时考虑到这一点。
硬件支持要求包括
•没有支持。 例如,hazard 指针的读取和写入可能不是原子的。16
•非通用原子操作(例如
•
•LL 和 SC 对(例如,larx和stcx在 IBM Power 架构上)。 Herlihy 也证明了这些是通用操作。 如前所述,在 CAS 容易受到 ABA 问题影响的某些情况下,它们对 ABA 问题免疫。 因此,
•硬件事务内存。 最近,IBM(Blue Gene/Q21、System Z12 和 Power3)和 Intel11 架构正在为任意内存事务提供硬件支持。 然而,大多数这些 HTM 架构(IBM System Z 除外)都是
请注意,如果可以并发执行某些操作的线程数受到限制,则非通用原子操作可能足以设计无锁 (lock-free) 实现。
除了原子操作的各种要求外,非阻塞算法在
在 Java 5 (2004) 和 C11/C++11 之前,这些语言无法可靠地使用(按照其标准指定)来完全表达所需的内存排序。 程序员和自定义库编写者依赖于汇编语言和机器二进制代码来表达此类排序。
现在,借助 C11/C++11 和 Java(5 及更高版本),程序员可以将某些变量指定为受特殊排序约束(Java 中的 volatiles 和 C11/C++11 中的 atomics)。 在某些情况下,这些指定可能
此类语言的实现在不同的硬件平台上具有不同的开销。 非阻塞实现的设计人员应考虑
数据结构和算法的组合选择是一个问题,可以在其中做出重大妥协,以设计一个满足其总体要求的系统。
非阻塞算法在其可移植性(例如,对特殊硬件支持的要求)、可靠性(例如,是否被广泛使用)、复杂性(例如,实现、维护和修改的合理易用性)、进展保证(例如,
以下示例的目的是演示本文中讨论的非阻塞特性和问题之间的相互作用。
考虑一个简化的
考虑到记录可以在数据结构之间来回移动,任何非阻塞算法都必须处理 ABA 问题。 由于记录可能很大且大小可变,因此限制其内存的重用不是一个可接受的选项。 经典的 ABA 解决方案(在
这可能是考虑妥协的好时机。 如何为每个记录添加一个单独的描述符? 描述符保存键值和遍历数据结构所需的任何字段; 然后可以在适当位置更改这些字段,而无需从结构中删除和添加记录。 通过这种妥协,完整记录的内存仍然可以释放,但可能可以接受阻止描述符被释放以进行任意重用。
现在让我们重新考虑 ABA 问题。 也许经典的
现在,设计人员的任务是平衡顺序性能(即,当使用带有
正如读者所见,本案例研究经历了对各种选择的几次迭代,从绝对必要的开始,做出妥协(使用描述符并添加间接级别),最后达成一组合理的选项,以供进一步考虑其优缺点。
本文介绍了关键问题,这些问题可以帮助非阻塞系统的设计者做出
作者感谢 Samy Al Bahra 和 Paul McKenney 对本文早期版本提出的深刻见解。
1. Attiya, H., Hendler, D. 2005. 使用以下技术的实现的时间和空间下界
2. Brown, P. J., Smith, R. M. 1973. 美国专利 3,886,525。由多个用户控制的共享数据(1973年6月提交)。
3. Cain, H. W., Frey, B., Williams, D., Michael, M. M., May, C., Le, H. 2013. Power 架构中事务内存的稳健架构支持。/IEEE 第40届国际计算机体系结构研讨会 (ISCA)。
4. Dragojevic, A., Herlihy, M., Lev, Y., Moir, M. 2011. 论硬件事务内存简化内存管理的能力。第30届年度
5. Fatourou, P., Kallimanis, N. D. 2011. 一种高效的
6. Heller, S., Herlihy, M., Luchangco, V., Moir, M., Scherer III, W. N., Shavit, N. 2005. 一种惰性并发的
7. Herlihy, M. 1991。
8. Herlihy, M., Luchangco, V., Martin, P. A., Moir, M. 2005. 对以下项的非阻塞内存管理支持
9. Herlihy, M., Luchangco, V., Moir, M. 2003。
10. IBM System/370 操作原理。1975。
11. Intel 架构指令集扩展编程参考。2012。
12. Jacobi, C., Slegel, T. J., Greiner, D. F. 2012. IBM System Z 的事务内存架构和实现。第45届年度 IEEE/ 国际微体系结构研讨会
13. McKenney, P. 2013. 结构化延迟:通过拖延进行同步。 队列。 https://queue.org.cn/detail.cfm?id=2488549
14. McKenney, P. E., Slingwine, J. D. 1998。
15. Michael, M. M., Scott, M. L. 1996. 简单、快速且实用的非阻塞和阻塞并发队列算法。第15届年度 分布式计算原理研讨会论文集
16. Michael, M. M. 2004. 危害指针:安全的内存回收,用于
17. Michael, M. M. 2004. 可扩展的
18. Michael, M. M. 2004. 实用的
19. Timnat, S., Braginsky, A., Kogan, A., Petrank, E. 2012。
20. Valois, J. D. 1995。
21. Wang, A., Gaudet, M., Wu, P., Amaral, J. N., Ohmacht, M., Barton, C., Silvera, R., Michael, M. M. 2012. Blue Gene/Q 硬件对事务内存支持的评估。在第21届国际并行体系结构与编译技术会议论文集中
喜欢它,讨厌它?请告诉我们 [email protected]
Maged M. Michael 是 IBM Thomas J. Watson 研究中心的科研人员。他获得了罗切斯特大学计算机科学博士学位。他的研究领域包括并发算法、非阻塞同步、事务内存、多核系统和并发内存管理。他是 杰出科学家,也是康涅狄格州科学与工程院院士。
© 2013
最初发表于 Queue 卷 11,第 7 期—
在 数字图书馆 中评论本文
Adam Morrison - 多核程序中的可扩展同步
为现代多核处理器设计软件带来了一个难题。传统的软件设计中,线程操作共享数据,其可扩展性有限,因为对共享数据更新的同步会串行化线程并限制并行性。另一种替代的分布式软件设计,其中线程不共享可变数据,消除了同步并提供了更好的可扩展性。但是,分布式设计使得实现共享数据结构自然提供的功能(例如动态负载平衡和强一致性保证)变得具有挑战性,并且并非适合每个程序。然而,通常,共享可变数据结构的性能受到当今使用的同步方法的限制,无论是基于锁还是无锁。
Fabien Gaud, Baptiste Lepers, Justin Funston, Mohammad Dashti, Alexandra Fedorova, Vivien Quéma, Renaud Lachaize, Mark Roth - 现代 NUMA 系统上内存管理的挑战
现代服务器级系统通常构建为将多个多核芯片组合在一个系统中。每个芯片都有一个本地 DRAM(动态随机存取存储器)模块;它们一起被称为一个节点。节点通过高速互连连接,系统完全一致。这意味着,对程序员透明地,一个核心可以向其节点的本地内存以及其他节点的内存发出请求。关键的区别在于远程请求将花费更长的时间,因为它们会受到更长的线路延迟的影响,并且可能必须在遍历互连时跳过多个跃点。
Spencer Rathbun - 使用 Promise 的并行处理
在当今世界,有很多理由编写并发软件。提高性能和增加吞吐量的愿望导致了许多不同的异步技术。然而,所涉及的技术通常很复杂,并且是许多细微错误的原因,尤其是在它们需要共享可变状态的情况下。如果不需要共享状态,那么这些问题可以通过更好的抽象(称为 promise)来解决。这些 promise 允许程序员将异步函数调用连接在一起,等待每个函数返回成功或失败,然后再运行链中的下一个适当的函数。
Davidlohr Bueso - 实用同步原语的可扩展性技术
在理想的世界中,应用程序有望在越来越大的系统上执行时自动扩展。然而,在实践中,不仅这种扩展不会发生,而且在更大的系统上看到性能实际上恶化是很常见的。