线程模型

本文翻译自http://tutorials.jenkov.com/java-concurrency/concurrency-models.html,机翻加人工校正,仅供学习交流。

线程模型

并发系统可以使用不同的并发模型实现。并发模型详细说明系统中的线程如何协作完成给它们的任务。不同的并发模型以不同的方式分割任务,线程可能以不同的方式进行交流和合作。这个并发模型教程会深入讲解在本文撰写时(2015 - 2019)使用地最受欢迎的并发模型。

并发模型和分布式系统的相似性

本文中描述的并发模型类似于用于分布式系统的不同的结构。在并发系统中不同的线程相互通信。在分布式系统中不同进程相互通信(可能在不同的电脑)。线程和进程在本质上非常类似的,这就是为什么不同的并发模型通常类似于不同的分布式系统架构。
当然分布式系统有额外的挑战,例如网络可能会失败、远程计算机或进程丢失等。但一个运行在服务器的并发系统可能会遇到类似的问题,例如CPU出问题,网卡出问题,磁盘出问题等。虽然出问题的概率可能较低,但它可以在理论上仍然发生。
因为并发模型类似于分布式系统架构,他们常常可以互相借鉴。例如,在工作者(线程)之间分配工作的模型通常类似于分布式系统的负载均衡。如日志、故障转移、幂等性的任务等的错误处理技术也是一致的。

共享状态vs.独立状态

并发模型的一个重要方面,组件和线程被设计为不是在线程之间处于共享状态,就是在线程之间处于独立状态。
共享状态意味着系统中不同的线程将共享一些状态。状态意味着是一些数据,通常是一个或多个对象或类似物。当线程共享状态时,竞态条件和死锁等问题可能发生,当然它取决于线程如何使用和访问共享对象。
在这里插入图片描述
独立状态意味着系统中不同的线程不共享任何状态。当不同的线程需要沟通的,他们通过交换不可变对象,或通过发送对象的副本(或数据)。因此,当两个线程写入同一个对象(数据/状态),可以避免最常见的并发性问题。
在这里插入图片描述
当你知道只有一个线程会修改给定的对象时,使用一个独立状态并发设计往往可以使用更容易思考和更容易实现的代码部分,不必担心并发访问该对象。当然,你可能会觉得使用独立状态去设计全局应用程序是有点困难的。我感觉是值得的,就我个人而言,我更喜欢独立状态并发设计。

并行工作

第一个并发模型是我称之为并行工作模型。传入的工作分配给不同的工作者。这是一个图说明并行工作并发模型:
在这里插入图片描述
在并行工作并发模型上一位全权代表分发的工作给不同的工作者,每个工作者单独完成全部工作,工作者们可能在不同的线程中或者不同的CPU上并行工作。
如果并行的工作模型对应汽车工厂,则每辆车将由一名工人制造。汽车的工人会得到详细说明书,并从开始到结束创造汽车的所有部件。
并行工作并发模型是在Java应用程序中最常用的并发模型(尽管这种情况正在改变)。许多在java.util.concurrent Java 包内的并发实用程序被设计为使用这个模型。你也可以看到这种模式在Java Enterprise Edition应用服务器的设计的痕迹。
并行工作并发模型可以同时使用共享状态或单独的状态,这意味着工人可以访问一些共享状态(共享对象或数据),或者他们不共享状态。

并行工作的优势

并行工作的并发模型的优势是它很容易理解。增加应用程序的并行化你只需要水平地添加更多的工作者。
举例来说,如果你写一个网络爬虫,你可以使用不同数量的爬虫爬一定数量的页面,看哪个数量的总爬行时间最短的(这意味着最高的性能)。由于web爬行是一个IO密集型工作,你可能在你的电脑上会以每个CPU /核心只有几个线程而告终。每个CPU一个线程会是太少,会导致闲置的很多时间等待数据下载。

并行工作的缺点

并行工作的并发模型有一些缺点隐藏在简单的表面下,。我将解释在以下部分中最明显的缺点。

共享状态变得复杂

为了共享工作者需要访问不是在内存中的共享数据,就是在共享数据库中的共享数据,管理正确的并发访问可能会变得很复杂。下面的图显示了如何使并行工作的并发模型:
在这里插入图片描述
其中一些共享状态像队列存在于通信机制中,但另一些共享状态是业务数据、数据缓存、到数据库的连接池等。
一旦这样共享状态在并行工作并发模型中,模型就会变得复杂起来。线程需要确保一个线程的更改对其他线程可见的这样的方式来访问共享数据(推到主内存,而不只是停留在执行线程的CPU缓存中)。线程需要避免竞态条件、死锁和许多其他共享状态并发性问题。
此外,当线程在访问共享时相互等待时,部分并行化会丢失。许多并发数据结构是阻塞的,这意味着只有一个或有限的一组线程可以在任何给定时间访问数据。这可能导致对这些共享数据结构的争用,高竞争会导致一定程度的访问共享的数据结构的代码的一部分执行串行化(消除并行化)。
现代非阻塞并发算法可能会减少争用和提高性能,但非阻塞算法很难实现。
持久数据结构是另一种选择。持久数据结构修改时总是保留以前版本。因此,如果多个线程指向同一个的持久数据结构,当一个线程修改它,修改线程获得新数据结构的引用。所有其他线程保持引用旧的结构仍然不变,从而保持一致。Scala标准api包含多个持久数据结构。
虽然持久数据结构是一个优雅的并发修改共享数据的解决方案,但持久数据结构通常性能不会很好。
例如,持久列表将把所有新元素添加到列表的头部,并返回新添加元素的引用(然后指向列表的其他部分)。所有其他线程还是以前的引用列表中的第一个元素,对于这些这些线程列表,看起来没有任何改变出,他们不能看到新添加的元素。
这样一个持续的列表是用链表实现的。不幸的是链表在现代的硬件上执行得不是很好。列表中的每个元素是一个单独的对象,这些对象可以分散在计算机的所有内存中。现代的CPU访问顺序数据要比随机数据快得多,所以在现代硬件上在数组之上实现列表将获得更高的性能。数组按顺序存储数据。CPU缓存可以一次加载很大的数组块到缓存中,一旦加载,CPU是可以直接访问CPU缓存中的数据。对于链表来说,这是不可能的,因为链表中的元素分散在RAM中。

无状态工作者

共享状态可以被系统中的其他线程修改。因此,工作者们每次需要时都必须重新读取这份状态,以确保他们正在使用最新的版本。无论共享状态是保存在内存中还是保存在外部数据库中。不在内部保存状态(但每次需要时都会重新读取它)的工作者称为无状态工作者。
每次需要时都重新读取数据可能会变慢。特别是当状态存储在外部时。

工作顺序的不确定性

并行工作模型的另一个缺点是工作执行顺序是不确定的。没有办法保证最先或最后执行哪些作业。工作A可以在工作B之前交给一个工作者,但是工作B可能回在工作A之前执行。
在任何给定的时间点并行工作模型的不确定性很难推断系统的状态。这也使得保证一个任务在另一个任务之前完成变得更加困难(如果不是不可能的话)。然而,这并不总是会引起问题。这取决于系统的需要。

流水工作线

第二个并发模型我称之为流水线并发模型。我选择这个名字只是为了与之前的“并行工作者”比喻相吻合。其他开发人员使用其他名称,例如反应式系统或事件驱动系统,这取决于平台/社区。下图说明了流水线并发模式:
在这里插入图片描述
工作者像工厂装配线上的工人一样被组织起来。每个工人只完成全部工作的一部分,当这部分工作完成后,工人将工作转发给下一个工人。使用流水线并发模型的系统通常被设计为使用非阻塞IO。非阻塞IO意味着当一个工人开始一个IO操作(例如,读取一个文件或数据从一个网络连接),工人不等待IO调用完成。IO操作很慢,所以等待IO操作完成是对CPU时间的浪费,CPU可以同时在做其他事情。当IO操作完成时,IO操作的结果(例如读取数据或写入数据的状态)被传递给另一个工人。
对于非阻塞IO, IO操作决定了工人之间的边界。在必须启动IO操作之前,工人会做尽可能多的工作,然后放弃对工作的控制去进行IO操作。当IO操作完成时,装配线上的下一个工人继续对该作业进行工作,直到必须启动IO操作。
在这里插入图片描述
实际上,这些工作可能不会沿着一条装配线进行。由于大多数系统可以执行多个作业,因此作业根据不同的作业下一步需要执行的工作的哪一部分,从一个工人流动到另一个工人。在现实中,可能有多条不同的虚拟装配线同时运行。在现实中,流水线系统中的作业流程是这样的:
在这里插入图片描述
作业甚至可以被转发到多个worker进行并发处理。例如,作业可以同时转发给作业执行程序和作业记录器。这个图表说明了这三条装配线是如何把它们的工作交给同一名工人完成的(装配线中间的最后一个工人)。
在这里插入图片描述
装配线可能比这还要复杂。

反应性、事件驱动系统

使用装配线并发模型的系统有时也被称为反应系统,事件驱动系统 。系统的工作人员对系统中发生的来自外部世界或来自其他工人的事件做出反应。事件的例子可以是传入的HTTP请求或者某个文件已经加载到内存中等等。
在撰写本文时,有许多有趣的响应式/事件驱动平台已经可用,未来还会有更多。一些比较受欢迎的似乎是:

  • Vert.x
  • Akka
  • Node.JS (JavaScript)
  • 我个人认为Vert.x很有趣(尤其是对于 JAVA /JVM 落伍的我)
角色vs.通道

角色和通道是装配线(或反应/事件驱动)模型的两个类似例子
在角色模型中,每个工作者都称为一个角色。角色之间可以直接发送消息。 消息是异步发送和处理的。正如上文描述,角色可用于实现一个或多个作业处理装配线,下图展示了角色模型:
在这里插入图片描述
在通道模型中,工作者之间不直接通信。它们在不同的通道上发布消息(事件)。然后,其他工作者可以监听这些通道上的消息,而不知道发送者是谁。下图展示了通道模型:
在这里插入图片描述
在我写这篇文章的时候,通道模式似乎更灵活了。一个工人不需要知道什么工人将在以后的装配线上处理工作,它只需要知道将作业转发到(或将消息发送到等)哪个通道。频道上的监听器可以订阅和取消订阅不影响工人发送消息给通道。这使得工人之间的耦合度更加松散。

流水线的优点

与并行工作并发模型相比,流水线并发模型有几个优点。我将在下面的部分中介绍最大的优点。

没有共享状态

工人不与其他工人共享状态的事实意味着它们可以不用考虑所有可能来自对共享状态的并发访问引起的并发问题。这使得实现工作者变得更加容易。在实现工作者时,就好像它是唯一执行该工作的线程——本质上是一个单线程。

有状态工作者

因为工作线程知道没有其他线程修改它们的数据,所以工作线程可以是有状态的。有状态的意思是它们可以将需要操作的数据保存在内存中,只有写操作将更改回最终的外部存储系统。因此,有状态工作者通常比无状态工作者速度更快。

更好的硬件整合

单线程程序代码的优点是更好地符合与底层硬件工作。首先,在单线程模式下执行,你通常可以创造更多的优化的数据结构和算法。第二,单线程有状态的工人可以在内存中缓存数据。当数据缓存在内存中,会有一个数据被缓存在执行线程的CPU的CPU缓存中的高概率。这使得访问缓存的数据更快。
当代码以一种自然受益的方式编写时,我将其称为硬件一致性,有些开发者称之为机制偏爱。我更喜欢硬件一致性这个术语,因为计算机只有很少的机械部件。"同情"这个词在这里是用来比喻"更般配"的,我认为“一致”这个词表达得相当好。不管怎样,这是吹毛求疵,使用你喜欢的任何术语。

工作顺序可能性

根据装配线并发模型实现并行系统是可能的,作业排序使得推理任何给定时间点的系统状态变得容易得多。此外,您可以将所有传入的作业写入日志。然后,以防系统的任何部分出现故障,可以使用此日志从头开始重新构建系统状态。作业按照一定的顺序写入日志,这个顺序就变成了保证工作顺序。下面是这样的设计:
在这里插入图片描述
执行有保证的工作顺序不一定容易,但通常是可能的。如果可以,通过完全通过日志文件极大地简化了备份、恢复数据、复制数据等任务

流水线的缺点

装配线并发模型的主要缺点是作业的执行是经常的,从而在项目中的多个类。因此,对于给定的作业,很难确切地看到正在执行什么代码。
编写代码也可能更加困难。工作代码有时被编写为回调处理程序,带有许多嵌套回调处理程序的代码可能会导致某些开发人员所称的回调地狱。回调地狱的意思就是很难跟踪代码在所有回调中真正在做什么,还要确保每个回调都能访问它需要的数据。
有了并行工作并发模型,这就容易多了。您可以打开工作代码并阅读从头到尾执行的代码。当然,并行工作代码也可以分布在许多不同的类中,但是执行序列通常更容易从代码中读取。

函数并行性

函数并行是这是最近被谈论很多(2015年)的第三种并发模式。函数并行性的基本思想是使用函数调用来实现程序。函数可以看作是相互发送消息的“代理”或“参与者”,就像装配线并发模型(又名反应性或事件驱动系统)中一样。当一个函数调用另一个函数时,这类似于发送消息。
复制传递给函数的所有参数,因此,接收函数之外的任何实体都不能操作数据。这种复制对于避免共享数据上的竞争条件至关重要。这使得函数的执行类似于原子操作。每个函数调用都可以独立于任何其他函数调用执行。
当每个函数调用都可以独立执行时,每个函数调用可以在单独的cpu上执行。这意味着 一个算法在功能上可以在多个cpu上并行执行。
在Java 7中,我们得到了包含实现类似于函数并行性的ForkAndJoinPool的Java .util.concurrent包。在Java 8中,我们得到了并行流,它可以帮助你并行化大型集合的迭代。请记住,有些开发者对ForkAndJoinPool持批评态度(你可以在我的ForkAndJoinPool教程中找到批评的链接)。
函数并行性的难点在于知道应该并行化哪个函数调用。跨CPU协调函数调用会带来开销,函数所完成的工作单元需要具有一定的规模,才能抵消这种开销。如果函数调用非常小,尝试并行化它们实际上可能比单线程、单CPU执行要慢。
就我理解 (这一点也不完美),你可以使用响应式、事件驱动的模型来实现算法和实现类似于通过函数并行实现工作的分解。使用事件驱动模型,你可以更好地控制并行化的内容和程度(在我的观点里)。
此外,将一个任务拆分到多个CPU上会产生协调的开销,只有当该任务是程序当前唯一正在执行的任务时才有意义。但是,如果系统同时执行多个其他任务例如例如,web服务器、数据库服务器和许多其他系统,试图将单个任务并行化是没有意义的。计算机中的其他CPU无论如何都要忙于其他任务,所以,我们没有理由用一个更慢的、功能上并行的任务来打断他们。使用装配线(反应性)并发模型可能会更好,因为它的开销更小(以单线程模式顺序执行)并且更好地符合底层硬件的工作方式。

哪种并发模型最好?

那么,哪种并发模型更好呢?
通常的情况是 答案取决于您的系统应该做什么。如果您的工作是自然并行的,独立的,没有必要去共享状态,您可能能够使用并行工作模型来实现您的系统。
然而,许多工作并不是自然地平行和独立的。对于这些系统我认为流水线并发模型的优点多于缺点,而且比并行工作模式有更多的优势。
你甚至不需要自己编写所有的流水线基础设施。现代平台像 Vert.x 为你实现了很多。就我个人而言,我会探索在 Vert.x平台之上的设计运行我的程序。我觉得Java EE已经没有优势了。

下一章:同一线程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值