同步锁同步代码块同步
同步是一个神话。 没有任何瞬间发生。 一切都需要时间。
计算系统和编程环境的某些特征从根本上建立在这样一个事实之上,即计算发生在一个三维的物理世界中,该三维世界由光速和热力学定律强加。
物理世界的这种基础意味着,即使具有新性能点和功能的新技术可用,某些方面仍然保持相关性。 这些原则是正确的,因为它们不仅是“设计选择”。 它们是由物理宇宙的基本现实驱动的。
语言和系统设计中的同步与异步之间的区别是具有深厚物理基础的设计领域的一个示例。 最初,大多数程序员都使用假定有同步行为的基本程序和语言。 实际上,它是如此自然,以至于甚至都没有明确地命名或描述它。 在此上下文中,术语“同步”的含义是,计算是按一系列连续步骤立即进行的,直到计算完成后才进行其他操作。 我执行“ c = a + b”或“ x = f(y)”,直到语句执行完成,才发生其他情况。
当然,在物理宇宙中,什么也不会立即发生。 一切都涉及一些延迟,以导航内存层次结构,执行处理器周期,从磁盘驱动器读取数据或以一定的延迟在某些通信网络上进行通信。 这是光速和三维的基本结果。
一切都涉及一些延迟,消耗一些时间。 如果我们将某些事物定义为同步的,则实际上是在说我们将忽略该延迟,并将其描述为瞬时发生。 实际上,计算机系统经常分配大量的基础结构来继续利用底层硬件,即使它们试图通过将行为视为同步来公开更方便的编程接口。
同步涉及机制和开销的想法对于程序员来说似乎是违反直觉的,因为程序员更习惯于将异步视为需要大量显式机制。 实际上,公开异步接口时实际上发生的是,程序员将更多真正的基础异步信息公开给程序员,以手动进行处理,而不是让系统自动处理。 直接暴露异步给程序员带来了更多的开销,但也允许自定义成本和权衡问题领域,而不是让系统平衡这些成本并进行权衡。 异步接口通常更直接地映射到底层系统中实际发生的事情,并提供其他优化机会。
例如,处理器和内存系统具有适当的基础架构,可以处理通过内存层次结构读取或写入数据。 一级(L1)缓存内存引用可能需要几纳秒,而需要一直导航到L2,L3和主内存的内存引用可能需要100纳秒。 简单地等待内存引用解析将使处理器大部分时间处于闲置状态。
优化这一点的机制很重要; 通过在指令流中向前看并同时进行多个内存获取和存储来进行流水线化,分支预测以尝试继续优化该执行,即使程序跳转到新位置,也要仔细管理内存屏障以确保所有这些复杂操作机制继续将一致的内存模型公开给更高级别的编程环境。 所有这些都试图优化性能并最大程度地利用硬件,同时在内存层次结构中隐藏这10–100 ns纳秒的延迟,从而有可能公开看起来像同步执行的内容,并且仍然可以从核心处理引擎中获得良好的性能。
这些优化对于特定的代码是否有效尚不明确,并且往往需要高度专业的性能工具进行分析。 此类分析工作仅用于有限类型的高价值代码(例如,Excel重新计算引擎或某些核心压缩或密码代码路径)。
从旋转磁盘读取数据等更高的延迟需要不同的机制。 在这种情况下,从磁盘读取的请求将使OS完全切换到其他线程或进程,同时未处理同步请求。 这种机制的高线程切换成本和开销是可以接受的,因为隐藏的等待时间约为毫秒,而不是纳秒。 请注意,这些成本不仅是在运行线程之间进行切换的成本,而且还包括有效闲置和冻结直到该操作完成的所有内存和其他资源的成本。 这些成本都是为了公开此同步接口而产生的。
有很多根本原因,为什么系统可能想要公开真正的底层异步,以及为什么某些组件,层或应用程序更愿意使用异步接口,即使以直接管理增加的复杂性为代价。
并行性。 如果公开的资源具有真正的并行能力,则异步接口可以使客户端更自然地一次发出和管理多个未完成的请求,并可以更充分地利用基础资源。
流水线。 减少某些接口的有效等待时间的一种常用方法是一次处理多个请求(这是否真正对提高性能有用,取决于等待时间的来源)。 无论如何,如果系统适合流水线工作,则可以将有效等待时间减少等于进行中的未完成请求数的因数。 因此,单个请求可能需要10毫秒才能完成,但是用10个请求填充管道可能会看到响应每1毫秒返回一次。 总吞吐量是可用流水线的函数,而不仅仅是单个端到端请求的延迟。 发出请求并等待响应的同步接口将始终看到较长的端到端延迟。
批处理(本地和远程)。 异步接口更自然地允许在本地或在远程资源上实现用于批处理请求的系统(请注意,在这种情况下,“远程”可能位于I / O接口另一端的磁盘控制器中)。 这是因为应用程序在继续其本地处理时已经需要管理一些延迟才能获得响应。 额外的处理可能涉及提出可以自然地一起批处理的额外请求。
在本地进行批处理可以更有效地传输一系列请求,甚至可以在本地压缩和删除重复的请求。 在远程资源处,同时访问整个请求集可以允许进行重大优化。 一个典型的例子是磁盘控制器对读取和写入序列进行重新排序,以利用磁盘头在旋转盘片上的位置,从而最大程度地减少平均寻道时间。 通过将一系列读取或写入同一块的请求分批处理,任何在块级别工作的存储接口都可以看到主要的性能改进。
当然,可以通过同步接口进行本地批处理,但是要么需要掩盖事实真相,要么要求批处理是接口的显式功能,这可能会增加客户端的复杂性。 缓冲IO是“遮蔽真相”的经典示例。 应用程序调用“ write(byte)”,并且接口返回成功,但实际上直到某些缓冲区被填充并刷新后,实际的写操作(以及是否成功完成的事实)才发生,显式刷新了缓冲区或在文件关闭时刷新。 许多应用程序可以忽略这些细节,只有当应用程序需要保证某些交互操作序列并且需要底层的真相时,它才会变得混乱。
解除阻止/解除绑定。 在图形用户界面的上下文中,异步的最常见角色之一是保持核心UI线程不受阻塞,以便应用程序可以继续与用户进行交互。 长时间运行的操作(例如与网络交互)的延迟不能隐藏在同步接口后面。 在这种情况下,UI线程需要显式管理这些异步操作并处理它带来的其他复杂性。
UI只是一个示例,其中组件需要保持对其他请求的响应,因此不能轻易使用隐藏延迟的某种通用机制来简化编程体验。 接收新套接字连接的Web服务器组件通常会将其快速移交给实际上管理套接字上的交互的其他一些异步组件,以便它可以返回到接收和处理新请求。
通常,同步设计倾向于将组件及其处理模型紧密地锁定在一起。 异步交互通常是一种控制耦合并允许松散绑定的机制。
减少和管理开销。 如上所述, 隐藏异步的任何机制都涉及一些资源分配和开销。 对于给定的应用程序来说,这种开销可能是不可接受的,并且将促使应用程序设计人员直接管理固有的异步性。
Web服务器的历史就是一个有趣的例子。 早期的Web服务器(基于Unix构建)会分叉一个单独的进程来管理传入的请求。 然后,该过程可以以大体上同步的方式读取和写入该连接。 该设计的改进减少了开销,但通过使用线程而不是进程来维护交互的总体同步模型。 当前的设计认识到设计的主要重点不是处理模型,而是由在制定响应时读写网络,数据库和文件系统所涉及的IO所主导。 他们通常使用工作队列,并限制在一些固定的线程集中,在这些线程集中可以更明确地管理资源使用情况。
NodeJS在后端开发中的成功不仅仅是因为它利用了成长为构建前端Web界面的大量Javascript开发人员。 像基于浏览器的脚本一样,NodeJS高度关注异步设计,该设计很好地映射到以IO而非处理为主的典型服务器资源负载。
这里另一个有趣的方面是,这些折衷方案更加明确,并且可以由应用程序设计人员使用异步设计进行调整。 在内存层次结构中的延迟示例中,有效延迟(以每个主内存请求的处理器周期为单位)在过去数十年中一直在急剧上升。 处理器设计人员疯狂地争先恐后地添加了额外的缓存层和其他机制,以扩展处理器公开的内存模型,以保持同步处理的功能。
同步IO边界处的上下文切换也是有效权衡随着时间而发生巨大变化的一个示例。 与处理器周期时间相比,IO延迟的改进要慢得多,这意味着当应用程序处于等待IO完成状态时,它放弃了更多的计算机会。 相同的相对成本权衡问题已促使OS设计人员转向内存管理设计,该设计看起来更像是早期的流程交换方法(在此流程开始执行之前,将整个流程内存映像加载到内存中),而不是按需分页。 隐藏每个页面边界可能发生的延迟变得非常困难。 通过较大的串行IO请求与随机请求相比,更好的聚合带宽也推动了这一特定变化。
其他话题
消除
取消是一个复杂的话题。 从历史上看,同步设计在处理取消方面做得很差,包括完全不支持取消。 它固有地必须建模为带外模型,并由单独的执行线程调用。 或者,异步设计更自然地支持取消,包括简单的方法,即简单地决定忽略最终(或不返回)任何响应。 取消随着延迟和实际错误率差异的增加而变得越来越重要-这说明了我们的网络环境随着时间的推移发展得相当不错。
节流/资源管理
同步设计通过在当前请求完成之前不允许应用程序发出其他请求,从而固有地实施了某些限制。 异步设计不会免费节流,可能需要显式解决。 这篇文章描述了Word Web App中的一个示例,其中从同步设计过渡到异步设计引入了重大的资源管理问题。 对于使用某些同步接口的应用程序来说,通常不会在代码中隐含节流,这是很正常的。 消除隐式节流可以(或强制)进行更明确的资源管理。
在将文档编辑器应用程序从Sun的同步图形API移植到X Windows时,我在职业生涯的早期就经历了这一过程。 使用Sun的API,绘图操作是同步执行的,因此客户端直到完成才获得控制权。 在X Windows中,图形请求是通过网络连接异步调度的,然后由窗口服务器(可能在同一台或不同的计算机上)执行。
为了提供良好的交互性能,我们的应用程序将进行一些绘制(例如,确保已更新并绘制了带有光标的当前行),然后检查是否还有更多键盘输入需要读取。 如果有更多输入,它将放弃绘制当前屏幕(在任何情况下,在处理未决输入后通常都会过时),以便读取和处理输入并以最新更改重绘。 调整它可以与同步图形API一起很好地工作。 异步接口接受绘图请求的速度比执行请求的速度快,从而使屏幕滞后于用户输入。 在提供图像的交互式拉伸时,这非常糟糕,因为发出请求的相对成本比实际执行请求的成本低得多。 UI将严重滞后于执行针对每个更新的鼠标位置发出的一系列不必要的重新绘制。
30多年后,这仍然是一个棘手的设计问题(Facebook的iPhone应用程序似乎在某些情况下正遭受此问题的困扰)。 一种替代设计是使屏幕刷新代码(它知道屏幕能够刷新的速度)作为驱动程序,并明确地回调给客户端以绘制区域,而不是由客户端来驱动交互。 这对客户端代码提出了严格的时间要求,以使其有效并且并不总是切合实际。
Windows历史记录
Windows做出了一个早期决定,即主要使用同步接口,并随着时间的推移将该核心方法扩展到数千个API中的100个。 用于访问本地PC资源的早期API通常可能会很快成功或失败,特别是相对于那些早期处理器的速度而言。 同步进程间API的确使Windows程序容易受到包装上较差的书面应用程序的攻击。 随着API扩展到网络和处理器的改进,隐藏在同步接口后面的延迟可能变得很大且变化很大,使得同步方法所涉及的权衡越来越不吸引人。
在API周围“包装线程”实际上可以使此同步接口变为异步状态,而这将占用大量资源,并且无法解决整个大型交互API集中更广泛的跨线程问题。 这是一种特别痛苦的方法,因为线程所做的唯一一件事就是有效地等待一个API完成,而该API本身只是坐在等待来自它所调用的较低级API的一些异步响应。 人们可能会看到具有数百个分配线程的应用程序,其中只有少数实际上正在执行任何实际工作-尽管事实上,线程本质上是处理的抽象,而不是等待。
随着时间的推移,使用线程池和其他技术已有一些改进。 Windows 8和Windows RunTime旨在清除大部分此类遗留物,但实际上已基于较旧的Win32 API的遗留持久性以及在其之上构建的大量代码而无效。
复杂
整个主题最终围绕使用异步设计来构建应用程序的相对挑战和复杂性。 在高级内部Microsoft技术研讨会上的第一次演讲中,我认为异步API是构建无法像当时的PC应用程序那样挂起的应用程序的关键。 在房间的后面,图灵奖得主,个人计算机之父之一巴特勒·兰普森大喊:“但是后来他们也不起作用”! 在接下来的几年中,我与Butler进行了许多富有成果的讨论,他仍然非常关注如何大规模管理异步。
异步设计引入了两个关键问题。 第一个是如何描述异步响应到达后如何重新启动计算。 特别是,关注点在于如何以一种可组合的方式做到这一点,以支持构建由独立组件组成的大型复杂应用程序所需的信息隐藏。 显式的事件驱动状态机很常见。 诸如异步/等待和诺言之类的语言技术适用于问题的这一部分。 更多“原始”方法(如使用回调)在Javascript代码中非常常见。 这些挑战在于,长期存在的程序状态会被埋在黑盒回调中,而黑盒回调通常实际上无法管理和管理。 Async / await允许将异步计算本质上描述为串行,同步代码。 在封面之下,这转化为一组闭包和延续功能。 该技术还提供了异步计算的标准包装器,而不是许多原始的基于回调的代码所采用的即席和不一致的方法。
这些方法都无法解决第二个关键问题。 本质上,问题在于如何隔离中间状态。 在同步设计中,计算过程中使用的任何中间状态都将消失,因此一旦同步计算返回,则将不可用。 异步设计与具有多个线程对共享数据进行操作的设计存在相同的问题(请注意,请注意)。 中间状态被(潜在地)暴露,从而以指数方式增加了总程序状态的有效数量以进行推理。
实际上,状态泄漏在两个方向上都发生。 内部状态可能会暴露给程序的其余部分,而不断变化的外部状态可能会在异步计算内部被引用。 诸如async / await之类的机制有意使代码看起来像串行同步代码,这无助于澄清此处的风险。 实际上,它们掩盖了风险。
与管理复杂性的一般设计挑战一样,隔离(尤其是数据隔离)是应用的关键策略。 图形应用程序的一个挑战是该应用程序很可能希望公开并提供有关这些中间状态的用户反馈。 一个共同的要求是证明在某些计算上取得了进展,或者中断并重新启动受到后续用户活动影响的计算(Word页面布局或Excel重新计算是这两个类别中的经典示例)。 多年来,在许多情况下,我试图确定这些附加程序状态应如何呈现给用户,而不是简单地放置一个等待光标,从而带来了许多额外的复杂性。 当然,对于可能花费很短时间或容易失败的操作,用户想知道到底发生了什么。
无论采用何种策略,纪律严明和始终如一最终都会随着时间的流逝而获得大笔红利。 临时方法很快变得复杂。
结论
世界是异步的。 同步是一个神话,也是一个昂贵的神话。 拥抱异步通常可以允许对系统的基础现实进行更明确的建模,并可以明确管理资源和成本。 复杂性来自不断扩展的程序状态以进行推理; 通过隔离状态并限制特定代码段可以看到的状态数来控制复杂性。
翻译自: https://hackernoon.com/synchrony-is-a-myth-708acaf479f
同步锁同步代码块同步