|
1 | 1 | [TOC]
|
2 | 2 |
|
3 | 3 | <!-- Concurrent Programming -->
|
| 4 | + |
4 | 5 | # 第二十四章 并发编程
|
5 | 6 |
|
6 | 7 | > 爱丽丝:“我可不想到疯子中间去”
|
@@ -89,47 +90,47 @@ slowdown occurs):
|
89 | 90 |
|
90 | 91 | ## 并发的超能力
|
91 | 92 |
|
92 |
| -想象一下,你置身于一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并沿着走廊向下移动。走廊分开了。 |
| 93 | +想象一下,你置身于一部科幻电影。你必须在一栋大楼中找到一个东西,它被小心而巧妙地隐藏在大楼一千万个房间中的一间。你进入大楼,沿着走廊走下去。走廊是分开的。 |
93 | 94 |
|
94 |
| -你自己完成这项任务需要一百个生命周期。 |
| 95 | +一个人完成这项任务要花上一百辈子的时间。 |
95 | 96 |
|
96 | 97 | 现在假设你有一个奇怪的超能力。你可以将自己一分为二,然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔到下一层时,你都会重复这个分裂的技巧。最终,整个建筑中的每个走廊的终点都有一个你。
|
97 | 98 |
|
98 |
| -每个走廊都有一千个房间。你的超能力变得有点弱,所以你只能分裂出 50 个自己来搜索这间房间。 |
| 99 | +每个走廊都有一千个房间。此时你的超能力变得弱了一点,一个房间只能分裂出 50 个人进去搜索。 |
99 | 100 |
|
100 |
| -一旦克隆体进入房间,它必须搜索房间的每个角落。这时它切换到了第二种超能力。它分裂成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能 - 一旦你开启它就会自动工作。在他们自己的控制下,纳米机器人开始行动,搜索房间然后回来重新组装成你,突然间,你获得了寻找的物品是否在房间内的消息。 |
| 101 | +一旦克隆体进入房间,它必须搜索房间的每个角落。这时它切换到了第二种超能力。它分裂成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能 - 一旦你开启它就会自动工作。在他们自己的控制下,纳米机器人开始行动,搜索房间然后回来重新组装成你,突然间,不知怎么的,你就知道这间房间里有没有那个东西。 |
101 | 102 |
|
102 | 103 | 我很想说,“并发就是刚才描述的置身于科幻电影中的超能力“就像你自己可以一分为二然后解决更多的问题一样简单。但是问题在于,我们来描述这种现象的任何模型最终都是泄漏抽象的(leaky abstraction)。
|
103 | 104 |
|
104 |
| -以下是其中一个漏洞:在理想的世界中,每次克隆自己时,你还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策 |
| 105 | +以下是其中一个漏洞:在理想的世界中,每次克隆自己时,还需要复制一个物理处理器来运行该克隆。这当然是不现实的——实际上,你的机器上一般只有 4 个或 8 个处理器核心(编写本文时的典型情况)。你也可能更多,但仍有很多情况下只有一个单核处理器。在关于抽象的讨论中,分配物理处理器核心这本身就是抽象的泄露,甚至也可以支配你的决策。 |
105 | 106 |
|
106 |
| -让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人回答。如果我们每个搜索者有一个处理器,这没有问题 - 处理器只是空闲,直到门被回答。但是如果我们只有 8 个处理器和数千个搜索者,我们不希望处理器仅仅因为某个搜索者恰好在等待回答中被锁住而闲置下来。相反,我们希望将处理器应用于可以真正执行工作的搜索者身上,因此需要将处理器从一个任务切换到另一个任务的机制。 |
| 107 | +让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人开门。如果每个搜索者都有一个处理器核心,这没有问题——只是空闲等待直到有人开门。但是如果我们只有 8 个处理器核心却有几千个搜索者,我们不希望处理器仅仅因为某个搜索者恰好在等待回答中被锁住而闲置下来。相反,我们希望将处理器应用于可以真正执行工作的搜索者身上,因此需要将处理器从一个任务切换到另一个任务的机制。 |
107 | 108 |
|
108 |
| -许多模型能够有效地隐藏处理器的数量,并允许你假装你的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。 |
| 109 | +许多模型能够有效地隐藏处理器的数量,允许你假装有很多个处理器。但在某些情况下,这种方法会失效,这时你必须知道处理器核心的真实数量,以便处理这个问题。 |
109 | 110 |
|
110 |
| -其中一个最大的影响取决于你是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。 |
| 111 | +最大的影响之一取决于您是使用单核处理器还是多核处理器。如果你只有单核处理器,那么任务切换的成本也由该核心承担,将并发技术应用于你的系统会使它运行得更慢。 |
111 | 112 |
|
112 |
| -这可能会让你认为,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上为了这个目的值得让它运行得更慢。 |
| 113 | +这可能会让你以为,在单核处理器的情况下,编写并发代码是没有意义的。然而,有些情况下,并发模型会产生更简单的代码,光是为了这个目的就值得舍弃一些性能。 |
113 | 114 |
|
114 |
| -在克隆体敲门等待的情况下,即使单处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备好的任务。但是如果所有任务都可以一直运行那么切换的成本反而会使任务变慢,在这种情况下,如果你有多个进程,并发通常只会有意义。 |
| 115 | +在克隆体敲门等待的情况下,即使单核处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备运行的任务。但是如果所有任务都可以一直运行那么切换的成本反而会使任务变慢,在这种情况下,如果你有多个进程,并发通常只会有意义。 |
115 | 116 |
|
116 |
| -假设你正在尝试破解某种密码,在同一时间内参与破解的线程越多,你越快得到答案的可能性就越大。每个线程都能持续使用你所分配的处理器时间,在这种情况下(一个计算约束问题),你的代码中分配的线程数应该和你拥有的处理器的数量保持一致。 |
| 117 | +如果你正在尝试破解某种密码,在同一时间内参与破解的线程越多,你越快得到答案的可能性就越大。每个线程都能持续使用你所分配的处理器时间,在这种情况下(CPU 密集型问题),你代码中的线程数应该和你拥有的处理器的核心数保持一致。 |
117 | 118 |
|
118 |
| -在接听电话的客户服务部门,你只有一定数量的员工,但是你的部门可能会收到很多电话。这些员工(处理器)一次只能处理一个电话,直到完成,与此同时,额外的电话必须排队。 |
| 119 | +在接听电话的客户服务部门,你只有一定数量的员工,但是你的部门可能会打来很多电话。这些员工(处理器)一次只能接听一个电话直到打完,此时其它打来的电话必须排队等待。 |
119 | 120 |
|
120 |
| -在“鞋匠和精灵”的童话故事中,鞋匠有很多工作要做,当他睡着时,出现了一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时也会产生限制 - 例如,鞋底需要大量的时间去制作,这会限制制鞋的速度并改变你设计解决方案的方式。 |
| 121 | +在“鞋匠和精灵”的童话故事中,鞋匠有很多工作要做,当他睡着时,出现了一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时也会产生限制——例如,如果鞋底的制作时间最长,这就限制了鞋子的制作速度,这也会改变你设计解决方案的方式。 |
121 | 122 |
|
122 |
| -因此,你尝试解决的问题驱动解决方案的设计。有一个迷人的抽象那就是将一个问题分解为子问题并且让它们独立运行,然后就是赤裸裸的现实。物理现实一次又一次地打了这种抽象的脸。 |
| 123 | +因此,你要解决的问题推动了解决方案的设计。将一个问题分解成“独立运行”的子任务,这是一种美好的抽象,然后就是残酷的现实:物理现实不断地侵入和动摇这个抽象。 |
123 | 124 |
|
124 |
| -这只是问题的一部分。考虑一个制作蛋糕的工厂。我们不知何故在工人中分发了蛋糕制作任务,但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子,准备存放蛋糕。但是,在工人将蛋糕放入盒子之前,另一名工人投入并将蛋糕放入盒子中!我们的工人已经把蛋糕放进去了,然后就开始了!这两个蛋糕被砸碎并毁了。这是常见的“共享内存”问题,产生我们称之为竞争条件的问题,其结果取决于哪个工作人员可以首先将蛋糕放入盒子中(通常使用锁机制来解决问题,因此一个工作人员可以先拿到盒子并防止蛋糕被砸烂)。 |
| 125 | +这只是问题的一部分:考虑一个制作蛋糕的工厂。我们以某种方式把制作蛋糕的任务分给了工人们,但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子,准备存放蛋糕。但是在工人把蛋糕放进盒子之前,另一个工人就冲过去,把蛋糕放进盒子里,砰!这两个蛋糕撞到一起砸坏了。这是常见的“共享内存”问题,会产生所谓的竞态条件(race condition),其结果取决于哪个工人能先把蛋糕放进盒子里(通常使用锁机制来解决问题,因此一个工作人员可以先抓住一个盒子并防止蛋糕被砸烂)。 |
125 | 126 |
|
126 |
| -当“同时”执行的任务相互干扰时,会出现问题。它可以以如此微妙和偶然的方式发生,可能公平地说,并发性“可以说是确定性的,但实际上是非确定性的。”也就是说,你可以假设编写通过维护和代码检查正常工作的并发程序。然而,在实践中,我们编写的并发程序似乎都能正常工作,但是在适当的条件下,将会失败。这些情况可能永远不会发生,或者在你在测试期间几乎很难发现它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。 |
127 |
| -这是学习并发中最强有力的论点之一:如果你忽略它,你可能会受伤。 |
| 127 | +当“同时”执行的任务相互干扰时,就会出现问题。这可能以一种微妙而偶然的方式发生,因此可以说并发是“可以论证的确定性,但实际上是不确定性的”。也就是说,假设你很小心地编写并发程序,而且通过了代码检查可以正确运行。然而实际上,我们编写的并发程序大部分情况下都能正常运行,但是在一些情况下会失败。这些情况可能永远不会发生,或者在你在测试期间几乎很难发现它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。这是学习并发中最强有力的论点之一:如果你忽略它,你可能会受伤。 |
128 | 128 |
|
129 |
| -因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管 Java 8 在并发性方面做出了很大改进,但仍然没有像编译时验证 (compile-time verification) 或受检查的异常 (checked exceptions) 那样的安全网来告诉你何时出现错误。通过并发,你只能依靠自己,只有知识渊博,保持怀疑和积极进取的人,才能用 Java 编写可靠的并发代码。 |
| 129 | +因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管 Java 8 在并发性方面做出了很大改进,但仍然没有像编译时验证 (compile-time verification) 或受检查的异常 (checked exceptions) 那样的安全网来告诉你何时出现错误。关于并发,你只能依靠自己,只有知识渊博、保持怀疑和积极进取,才能用 Java 编写可靠的并发代码。 |
130 | 130 |
|
131 | 131 | <!-- Concurrency is for Speed -->
|
132 | 132 | <!-- 不知道是否可以找到之前翻译的针对速度感觉太直了 -->
|
| 133 | + |
133 | 134 | ## 并发为速度而生
|
134 | 135 |
|
135 | 136 | 在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦。答案是“不,除非你的程序运行速度不够快。”并且在决定用它之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中。如果有一种方法可以在更快的机器上运行你的程序,或者如果你可以对其进行分析并发现瓶颈并在该位置替换更快的算法,那么请执行此操作。只有在显然没有其他选择时才开始使用并发,然后仅在必要的地方去使用它。
|
|
0 commit comments