多核架构是主流软件开发的一个转折点,因为它们迫使开发人员编写并行程序。在Queue之前的一篇文章中,Herb Sutter 和 James Larus 指出,“并发革命主要是一场软件革命。困难的问题不是构建多核硬件,而是以一种让主流应用程序能够从 CPU 性能持续指数增长中受益的方式来编程。” 1 在这个新的多核世界中,开发人员必须编写显式并行应用程序,这些应用程序可以利用每个后续多核世代将提供的越来越多的核心。
并行编程给开发人员带来了许多新的挑战,其中之一是同步多个线程对共享内存的并发访问。程序员传统上使用锁进行同步,但是基于锁的同步具有众所周知的弊端。简单的粗粒度锁不能很好地扩展,而更复杂的细粒度锁则有引入死锁和数据竞争的风险。此外,使用细粒度锁编写的可扩展库不能轻易地组合在一起,以保持可扩展性并避免死锁和数据竞争。
TM(事务内存)提供了一种新的并发控制构造,避免了锁的弊端,并显着简化了并发编程。它将数据库社区几十年来使用的成熟的并发控制概念带入主流并行编程。事务语言构造易于使用,并且可以引导程序扩展。通过避免死锁并自动允许细粒度并发,事务语言构造使程序员能够安全地从线程安全库中组合可扩展的应用程序。
尽管 TM 仍处于研究阶段,但它具有越来越大的势头将其推向主流。最近定义的高性能计算系统 (HPCS) 语言——Sun 的 Fortress、IBM 的 X10 和 Cray 的 Chapel——都提出了新的事务构造来代替锁。主流开发人员作为并行编程技术的早期采用者,已经密切关注 TM,因为它有可能提高程序员的生产力;例如,在 Epic Games 的 Tim Sweeney 在 2006 年 POPL(编程语言原理)研讨会上的主题演讲中指出,“手动同步……对于处理游戏模拟中的并发性来说是绝望地难以处理的”,并声称“事务是并发可变状态的唯一可行的解决方案。”2
尽管具有势头,但将事务引入主流仍然面临许多挑战。即使使用事务,程序员也必须克服并行编程挑战,例如查找和提取并行任务以及将这些任务映射到并行架构以实现高效执行。在本文中,我们将描述事务如何缓解程序员在使用锁时面临的一些挑战,并探讨系统设计者在编程语言中实现事务时面临的挑战。
内存事务是一系列内存操作,要么完全执行(提交),要么不起作用(中止)。3 事务是原子性的,意味着它们是一个全有或全无的操作序列。如果事务提交,则其所有内存操作都将作为一个单元生效,就好像所有操作都瞬间发生一样。如果事务中止,则其所有存储似乎都不生效,就好像事务从未发生过一样。
事务在隔离中运行,这意味着它的执行就好像它是系统上唯一运行的操作,并且好像所有其他线程都在它运行时被暂停。这意味着内存事务的存储效果在事务提交之前在事务外部不可见;这也意味着在它运行时没有其他事务的冲突存储。
事务给程序员提供了串行执行的错觉,并且它们给出了它们相对于系统中其他并发操作以单个原子步骤执行的错觉。程序员可以串行地进行推理,因为没有其他线程会执行任何冲突的操作。
当然,TM 系统实际上并没有串行地执行事务;否则,它将破坏并行编程的目的。相反,系统“在底层”允许多个事务并发执行,只要它仍然可以为每个事务提供原子性和隔离性即可。在本文的后面,我们将介绍实现如何在提供原子性和隔离性的同时仍然允许尽可能多的并发性。
为程序员提供 TM 优势的最佳方法是用新的语言构造(例如 atomic { B })替换锁,该构造将块 B 中的语句作为事务执行。第一类语言构造不仅为程序员提供了语法便利,而且还支持提供编译时安全保证的静态分析,并支持编译器优化以提高性能,我们将在本文后面讨论这一点。
图 1 说明了如何在面向对象语言(如 Java)中引入和使用原子语句。该图显示了线程安全映射数据结构的两种不同实现。该图的 A 部分中的代码显示了使用 Java 的 synchronized 语句的基于锁的映射。get() 方法只是将调用委托给底层的非线程安全映射实现,首先将调用包装在 synchronized 语句中。synchronized 语句获取由同步哈希映射的另一个字段中保存的互斥对象表示的锁。相同的互斥对象保护对该哈希映射的所有其他调用。
使用锁,程序员已显式地强制所有线程串行执行通过此同步包装器的任何调用。一次只有一个线程可以调用此哈希映射上的任何方法。这是粗粒度锁定的一个示例。以这种方式编写线程安全程序很容易——您只需用单个锁保护通过接口的所有调用,强制线程一次一个地在接口内部执行。
图 1 的 B 部分显示了相同的代码,使用事务而不是锁。此代码不是使用带有显式锁对象的 synchronized 语句,而是使用新的原子语句。此原子语句声明对 get() 的调用应以原子方式完成,就好像它是相对于其他线程以单个执行步骤完成的一样。与粗粒度锁定一样,程序员可以通过简单地用原子语句包装通过接口的所有调用来使接口线程安全。然而,程序员没有显式地强制一次一个线程执行对该哈希映射的任何调用,而是向系统声明该调用应以原子方式执行。系统现在承担保证原子性的责任,并在底层实现并发控制。
与粗粒度锁定不同,只要数据访问模式允许事务并发执行,事务就可以提供可扩展性。事务系统可以通过两种方式提供良好的可扩展性
事务可以以允许并发读取访问以及对不相交的细粒度数据元素(例如,不同的对象或不同的数组元素)的并发访问的方式来实现。使用事务,程序员可以获得这些形式的并发性,而无需在程序中显式地编码它们。
可以使用锁编写并发哈希映射数据结构,以便您可以同时获得并发读取访问和对不相交数据的并发访问。实际上,最近的 Java 5 库提供了一个 HashMap 版本,称为 ConcurrentHashMap,它正是这样做的。然而,ConcurrentHashMap 的代码比使用粗粒度锁定的版本要长得多且复杂得多。该算法由线程专家设计,并且在添加到 Java 标准之前经历了全面的公开评审过程。一般来说,编写像 ConcurrentHashMap 这样高度并发的基于锁的代码非常复杂且容易出错,从而给软件开发过程带来了额外的复杂性。
图 2 比较了 HashMap 的三个不同版本的性能。它绘制了在 16 路 SMP(对称多处理)机器上完成一组固定的插入、删除和更新操作所需的时间。4 正如数字所示,粗粒度锁定的性能不会随着处理器数量的增加而提高,因此粗粒度锁定不可扩展。然而,细粒度锁定和事务内存的性能随着处理器数量的增加而提高。因此,对于此数据结构,事务为您提供与细粒度锁定相同的可扩展性和性能,但编程工作量却显着减少。正如这些数字所表明的那样,事务将允许尽可能多的并发性的艰巨任务委托给运行时系统。
尽管使用细粒度锁构建的高度并发库可以很好地扩展,但开发人员在从这些库中组合更大的应用程序后不一定能保持可扩展性。例如,假设程序员想要执行一个复合操作,该操作将值从一个并发哈希映射移动到另一个并发哈希映射,同时保持线程始终在一个哈希映射或另一个哈希映射中看到键,但永远不在两者中都不存在的这种不变性。实现此目的需要程序员求助于粗粒度锁定,从而失去并发哈希映射的可扩展性优势(图 3A)。为了实现此问题的可扩展解决方案,程序员必须以某种方式重用隐藏在并发哈希映射实现内部的细粒度锁定代码。即使程序员可以访问此实现,从中构建复合移动操作也有引入死锁和数据竞争的风险,尤其是在存在其他复合操作的情况下。
另一方面,事务允许程序员安全地从库中组合应用程序,并且仍然实现可扩展性。程序员可以简单地将事务包装在复合移动操作周围(图 3B)。只要两个线程访问两个底层哈希映射结构中的不同哈希表桶,底层的 TM 系统就将允许两个线程并发执行移动操作。因此,事务允许程序员采用单独编写的可扩展软件组件,并将它们组合成更大的组件,这种方式仍然可以提供尽可能多的并发性,但又不会因并发控制而导致死锁。
通过提供回滚副作用的机制,事务使语言能够提供失败原子性。在基于锁的代码中,程序员必须确保异常处理程序在释放锁之前正确地恢复不变性。此要求通常会导致复杂的异常处理代码,因为程序员不仅必须确保临界区捕获并处理所有异常,还必须跟踪临界区内使用的数据结构的状态,以便异常处理程序可以正确地恢复不变性。在基于事务的语言中,如果未捕获的异常从其块中传播出来,则原子语句可以回滚事务的所有副作用(自动恢复不变性)。这显着减少了异常处理代码的数量并提高了鲁棒性,因为事务内部未捕获的异常不会损害程序的不变性。
事务内存将并发管理的负担从应用程序程序员转移到系统设计者。在底层,软件和硬件的组合必须保证来自多个线程的并发事务以原子方式和隔离方式执行。TM 系统的关键机制是数据版本控制和冲突检测。
当事务执行时,系统必须同时管理数据的多个版本。由挂起的事务之一生成的新版本只有在该事务提交时才会变为全局可见。由先前提交的事务生成的旧版本必须保留,以防挂起的事务中止。使用积极版本控制,事务内的写入访问会立即将新数据版本写入内存。旧版本缓存在撤消日志中。如果事务稍后提交,则无需进一步操作即可使新版本全局可见。如果事务中止,则必须从撤消日志中恢复旧版本,从而导致一些额外的延迟。为了防止其他代码观察到未提交的新版本(原子性丧失),积极版本控制需要在整个事务期间使用锁或等效的硬件机制。
惰性版本控制将所有新数据版本存储在写入缓冲区中,直到事务完成。如果事务提交,则通过从写入缓冲区复制到实际内存地址,新版本变为可见。如果事务中止,则无需进一步操作,因为新版本已隔离在写入缓冲区中。与积极版本控制相比,惰性方法仅在提交过程中才会发生原子性丧失。惰性方法的挑战,特别是对于软件实现,是在事务提交时引入的延迟以及在事务读取时首先搜索写入缓冲区以访问最新数据版本的需要。
当两个或多个事务并发地对相同数据进行操作,并且至少有一个事务写入新版本时,就会发生冲突。冲突检测和解决对于保证原子执行至关重要。检测依赖于跟踪每个事务的读集和写集,它们分别包括事务在其执行期间从中读取和写入的地址。我们在事务内首次读取地址时将其添加到读集中。类似地,我们在首次写入访问地址时将其添加到写集中。
在悲观冲突检测下,系统会在事务读取和写入数据时逐步检查冲突。冲突会被尽早检测到,并且可以通过暂停其中一个事务或中止一个事务并在稍后重试来处理。一般来说,悲观检测的性能取决于用于解决冲突的策略集,这些策略通常称为争用管理。一个具有挑战性的问题是检测多个事务之间反复出现或循环冲突,这些冲突可能会阻止所有事务提交(缺乏向前进展)。
另一种选择是乐观冲突检测,它假设冲突很少见,并将所有检查推迟到每个事务结束时。在提交之前,事务会验证没有其他事务正在读取它写入的数据或写入它读取的数据。乐观检测的缺点是冲突检测得太晚,超出了事务读取或写入数据的时间点。因此,就地暂停不是冲突解决的可行选项,并且可能会因中止而浪费更多工作。另一方面,乐观检测通过在发生冲突时简单地赋予提交事务优先级来保证在所有情况下都能向前进展。它还允许读取额外的并发性,因为对写入的冲突检查是在每个事务结束时执行的。乐观冲突检测不适用于积极版本控制。
冲突检测的粒度也是一个重要的设计参数。对象级检测接近于面向对象环境中程序员的推理。根据对象的大小,它还可以减少冲突检测所需的空间和时间开销。它的缺点是,当两个事务在大型对象(例如多维数组)中的不同字段上操作时,可能会导致假冲突。字级检测消除了假冲突,但需要更多的空间和时间来跟踪和比较读集和写集。缓存行级检测在假冲突的频率与时间和空间开销之间提供了折衷。不幸的是,缓存行和字不是语言级实体,这使得程序员难以优化其代码中的冲突,尤其是在托管运行时环境中,这些环境对用户隐藏数据放置。
TM 系统的最后一个挑战是嵌套事务的处理。鉴于基于库的编程趋势以及事务可以轻松安全地组合的事实,嵌套可能会频繁发生。早期的系统通过包含最外层的事务来自动扁平化嵌套事务。虽然简单,但扁平化方法禁止显式事务中止,这对于异常的失败原子性很有用。另一种选择是在嵌套事务的执行期间发生冲突或中止时支持部分回滚到嵌套事务的开始。它要求嵌套事务的版本管理和冲突检测独立于最外层事务的版本管理和冲突检测。除了允许显式中止之外,这种对嵌套的支持还为性能调优和控制事务与运行时或操作系统服务之间的交互提供了强大的机制。5
目前尚不清楚这些选项中的哪一个会导致最佳设计。需要通过原型实现和广泛的应用来进一步体验,以量化性能、易用性和复杂性之间的权衡。在某些情况下,设计选项的组合会导致最佳性能。例如,一些 TM 系统对读取使用乐观检测,对写入使用悲观检测,同时在数组的字级和对其他数据类型的对象级检测冲突。6 然而,任何 TM 系统都必须为关键结构(读集、写集、撤消日志、写入缓冲区)提供高效的实现,并且必须促进与优化编译器、托管运行时和现有库的集成。以下各节讨论如何使用软件和硬件技术来应对这些挑战。
STM(软件事务内存)完全在软件中实现事务内存,以便它可以在标准硬件上运行。STM 实现对事务代码块内部的所有共享内存读取和写入使用读写屏障(即,插入 Instrumentation)。Instrumentation 由编译器插入,并允许运行时系统维护数据版本控制和冲突检测所需的元数据。
图 4 显示了编译器如何在 STM 实现中转换原子构造的示例。A 部分显示了程序员编写的原子代码块,B 部分显示了编译器在事务块中 Instrumentation 代码。我们使用简化的控制流来方便演示。setjmp 函数检查点当前执行上下文,以便可以在中止时重新启动事务。stmStart 函数初始化运行时数据结构。对全局变量 a 和 b 的访问通过屏障函数 stmRead 和 stmWrite 进行调解。stmCommit 函数完成事务并使其更改对其他线程可见。事务在其执行期间定期进行验证,如果检测到冲突,则事务中止。在中止时,STM 库回滚事务执行的所有更新,使用 longjmp 恢复在事务开始时保存的上下文,并重新执行事务。
由于 TM 访问需要 Instrumentation,因此编译器需要生成可能从事务内部调用的任何函数的额外副本。此副本包含 Instrumentation 访问,并在从事务内部调用该函数时调用。事务代码可以由编译器进行大量优化——例如,通过消除对同一地址或不可变变量的屏障。7
读写屏障对事务记录进行操作,事务记录是指针大小的元数据,与事务可能访问的每条数据相关联。运行时系统还为每个事务维护一个事务描述符。描述符包含其事务的状态,例如读集、写集和积极版本控制的撤消日志(或惰性版本控制的写入缓冲区)。STM 运行时导出 API,允许语言运行时的其他组件(例如垃圾收集器)检查和修改描述符的内容,例如读集、写集或撤消日志。描述符还包含元数据,允许运行时系统推断读取或写入数据时的嵌套深度。这允许 STM 部分回滚嵌套事务。8
写屏障实现了不同形式的数据版本控制和写入的冲突检测。对于积极版本控制(悲观写入),写屏障获取与更新的内存位置对应的事务记录的独占锁,记住该位置的旧值在撤消日志中,并在适当位置更新内存位置。对于惰性版本控制(乐观写入),写屏障将新值存储在写入缓冲区中;在提交时,事务获取所有必需的事务记录的独占锁,并将值复制到内存。
读屏障也对事务记录进行操作,以检测冲突并实现悲观或乐观形式的读取并发。对于悲观读取,读屏障只需在读取数据项之前获取相应事务记录的读锁。乐观读取通过使用数据版本控制来实现;事务记录保存关联数据的版本号。9
STM 实现在两种情况下检测到冲突:读或写屏障发现事务记录被其他事务锁定;或者在使用乐观读取并发性的系统中,事务在定期验证期间发现其读集中某些事务记录的版本号已更改。在发生冲突时,STM 可以使用各种复杂的冲突解决方案,例如导致事务以随机方式退避,或中止并重新启动某些冲突事务。
STM 允许事务与语言环境的其余部分(例如垃圾收集器)集成。它们允许事务与工具(例如调试器)集成。它们还允许对性能调优进行准确的诊断。最后,STM 避免过早地将 TM 语义烘焙到硬件中。
与单线程上基于锁的代码相比,STM 实现可能会产生 40-50% 的开销。此外,如果 STM 实现必须保证事务代码和非事务代码之间的隔离,则会产生额外的开销。减少这种开销是一个活跃的研究领域。与其他形式的 TM 一样,STM 对于处理不可撤销的操作(例如 I/O 和系统调用)也没有令人满意的方法,它们也无法在事务中执行任意预编译的二进制文件。
事务内存也可以在硬件中实现,称为 HTM(硬件事务内存)。HTM 系统在事务代码中不需要读写屏障。硬件透明地管理数据版本并跟踪冲突,因为软件执行普通的读写访问。除了减少 Instrumentation 的开销之外,HTM 系统不需要在事务中使用的函数的两个版本,并且可以与调用未 Instrumentation 库例程的程序一起使用。
HTM 系统依赖于缓存层次结构和缓存一致性协议来实现版本控制和冲突检测。缓存观察处理器发出的所有读取和写入,可以缓冲大量数据,并且由于其关联组织而可以快速搜索。所有 HTM 系统都修改一级缓存,但该方法扩展到更低级别的缓存,包括私有缓存和共享缓存。
为了跟踪事务的读集和写集,每个缓存行都用 R 和 W 跟踪位进行注释,这些跟踪位分别在首次读取或写入行时设置。当事务提交或中止时,所有跟踪位都使用组或闪存重置操作同时清除。
缓存通过存储撤消日志的工作集或事务的数据缓冲区来实现数据版本控制。在积极版本控制下的缓存写入之前,我们检查这是否是此事务中对缓存行的首次更新(W 位重置)。在这种情况下,缓存行及其地址将使用对缓存的额外写入添加到撤消日志中。如果事务中止,则硬件或软件机制必须遍历日志并恢复旧的数据版本。10
在惰性版本控制中,由事务写入的缓存行通过设置其 W 位成为写入缓冲区的一部分。11 如果事务中止,则通过使所有设置了 W 位的缓存行无效来立即刷新写入缓冲区。如果事务提交,则通过重置所有缓存行中的 W 位,写入缓冲区中的数据将立即对系统的其余部分可见。
为了检测冲突,缓存必须使用在多核芯片中实现的缓存一致性协议来传达其读集和写集。悲观冲突检测使用现有系统中交换的相同一致性消息。12 在事务内的读取或写入访问时,处理器将请求对相应缓存行的共享或独占访问。该请求将传输到所有其他处理器,这些处理器在其缓存中查找此缓存行的副本。如果远程缓存具有同一行的副本,并且设置了 R 位(用于独占访问请求)或 W 位(用于任一请求类型),则会发出冲突信号。乐观冲突检测的操作类似,但会将对写入集中缓存行的独占访问请求延迟到事务准备提交为止。单个批量消息足以传达所有请求。13
即使 HTM 系统消除了事务执行的大部分开销来源,但它们仍然引入了额外的挑战。HTM 在缓存层次结构和一致性协议中需要的修改并非易事。处理器供应商可能不愿在事务编程变得普遍之前实现它们。此外,用于跟踪事务的读集、写集和写入缓冲区的缓存具有有限的容量,并且可能会在长事务中溢出。
长事务可能很少见,但它们仍然必须以保留原子性和隔离性的方式处理。从程序员的角度来看,对事务大小设置依赖于实现的限制是不可接受的。最后,对于深度嵌套的事务或发生中断、分页或线程迁移时,处理缓存中的事务状态具有挑战性。14
几种提出的机制虚拟化了有限的资源,并简化了 HTM 系统中的组织。一种方法是使用基于 Bloom 过滤器的签名来跟踪读集和写集。签名提供了集合的紧凑但不太精确(悲观)的表示,如果需要,可以轻松保存、恢复或传达。缺点是,不精确的表示会导致额外的假冲突,这可能会降低性能。另一种方法是将读集、写集和写入缓冲区映射到虚拟内存,并在缓存溢出时使用硬件或固件机制在缓存和内存之间移动数据。
另一种虚拟化技术是使用混合 HTM-STM 实现。事务开始时使用 HTM 模式。如果硬件资源超出,则事务将回滚并在 STM 模式下重新启动。15 混合 TM 的挑战是软件和硬件事务之间的冲突检测。为了避免需要代码的两个版本,可以通过操作系统提供混合 STM 系统的软件模式,并在内存页面的粒度上进行冲突检测。16
一种最终的实现方法是从STM系统开始,并提供一小部分关键机制,以针对其主要的开销来源17。这种方法被称为HASTM(硬件加速STM)。HASTM引入了两个基本的硬件原语:支持检测缓存行的首次使用,以及支持检测可能对缓存行进行的远程更新。这两个原语可以显著减少通用检测开销中的读取屏障以及乐观读取情况下的读取集验证时间。
使用锁来构建可扩展的并行应用程序是困难的,并且充满了陷阱。事务内存避免了许多这些陷阱,并允许程序员以安全且可扩展的方式构建应用程序。事务通过将困难的并发控制问题从应用程序开发人员转移到系统设计人员,从而提高了程序员的生产力。
在过去的三年中,TM吸引了大量的研究活动,并取得了显著的进展18。然而,在事务能够作为一流的语言构造进入主流之前,还有许多开放的挑战需要解决。
开发人员希望保护他们在现有软件中的投资,因此事务必须以增量方式添加到现有语言中,并且必须开发工具来帮助将现有代码从锁迁移到事务。这意味着事务必须与现有的并发特性(如锁和线程)组合使用。系统调用和I/O必须允许在事务内部进行,并且事务内存必须与环境中的其他事务资源集成。事务代码的调试和调优工具也是挑战,因为事务仍然需要调优才能实现可扩展性,并且使用事务仍然可能出现并发错误。
事务并非解决所有并行编程挑战的万能药。还需要其他技术来解决诸如任务分解和映射等问题。尽管如此,事务朝着使并行编程更容易迈出了坚实的一步。这一步显然将受益于新的软件和硬件技术。
有关该主题的更广泛报道,请参阅PACT '06(并行体系结构和编译技术)教程“多核环境中的事务编程”的幻灯片,该幻灯片可在http://csl.stanford.edu/~christos/publications/tm_tutorial_pact2006.zip获取。
ALI-REZA ADL-TABATABAI 是英特尔公司编程系统实验室的首席工程师。他领导一个团队,为未来的英特尔架构开发编译器和可扩展的运行时。他目前的研究集中在支持未来多核架构并行编程的语言特性上。
CHRISTOS KOZYRAKIS (http://csl.stanford.edu/~christos) 是斯坦福大学电气工程和计算机科学的助理教授。他的研究重点是并行计算机系统的体系结构、编译器和编程模型。他正在研究事务内存技术,这些技术可以大大简化普通开发人员的并行编程。
BRATIN SAHA 是英特尔公司编程系统实验室的高级研究员。他是下一代IA-32处理器中同步和锁定的架构师之一。他参与了多核处理器的高度可扩展运行时的设计和实现。作为其中的一部分,他一直在研究语言特性,例如事务内存,以简化并行编程。
最初发表于 Queue 杂志第4卷第10期—
在数字图书馆中评论本文
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 - 实用同步原语的可扩展性技术
在理想的世界中,应用程序有望在越来越大的系统上执行时自动扩展。然而,在实践中,不仅这种扩展不会发生,而且在更大的系统上,性能实际上变得更糟也很常见。