ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
<!-- Concurrency Superpowers --> ## 并发的超能力 想象一下,你置身于一部科幻电影。你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑物的一千万个房间之一中的单个物品。你进入建筑物并沿着走廊向下移动。走廊分开了。 你自己完成这项任务需要一百个生命周期。 现在假设你有一个奇怪的超能力。你可以将自己一分为二,然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔到下一层时,你都会重复这个分裂的技巧。最终,整个建筑中的每个走廊的终点都有一个你。 每个走廊都有一千个房间。你的超能力变得有点弱,所以你只能分裂出50个自己来搜索这间房间。 一旦克隆体进入房间,它必须搜索房间的每个角落。这时它切换到了第二种超能力。它分裂成了一百万个纳米机器人,每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能 - 一旦你开启它就会自动工作。在他们自己的控制下,纳米机器人开始行动,搜索房间然后回来重新组装成你,突然间,你获得了寻找的物品是否在房间内的消息。 我很想说,“并发就是刚才描述的置身于科幻电影中的超能力“就像你自己可以一分为二然后解决更多的问题一样简单。但是问题在于,我们来描述这种现象的任何模型最终都是泄漏抽象的(leaky abstraction)。 以下是其中一个漏洞:在理想的世界中,每次克隆自己时,你还会复制硬件处理器来运行该克隆。但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多,并且仍有许多情况只有一个处理器。在抽象的讨论中,物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策 让我们在科幻电影中改变一些东西。现在当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人回答。如果我们每个搜索者有一个处理器,这没有问题 - 处理器只是空闲,直到门被回答。但是如果我们只有8个处理器和数千个搜索者,我们不希望处理器仅仅因为某个搜索者恰好在等待回答中被锁住而闲置下来。相反,我们希望将处理器应用于可以真正执行工作的搜索者身上,因此需要将处理器从一个任务切换到另一个任务的机制。 许多模型能够有效地隐藏处理器的数量,并允许你假装你的数量非常大。但是有些情况会发生故障的时候,你必须知道处理器的数量,以便你可以解决这个问题。 其中一个最大的影响取决于你是单个处理器还是多个处理器。如果你只有一个处理器,那么任务切换的成本也由该处理器承担,将并发技术应用于你的系统会使它运行得更慢。 这可能会让你认为,在单个处理器的情况下,编写并发代码时没有意义。然而,有些情况下,并发模型会产生更简单的代码,实际上为了这个目的值得让它运行得更慢。 在克隆体敲门等待的情况下,即使单处理器系统也能从并发中受益,因为它可以从等待(阻塞)的任务切换到准备好的任务。但是如果所有任务都可以一直运行那么切换的成本反而会使任务变慢,在这种情况下,如果你有多个进程,并发通常只会有意义。 假设你正在尝试破解某种密码,在同一时间内参与破解的线程越多,你越快得到答案的可能性就越大。每个线程都能持续使用你所分配的处理器时间,在这种情况下(一个计算约束问题),你的代码中分配的线程数应该和你拥有的处理器的数量保持一致。 在接听电话的客户服务部门,你只有一定数量的员工,但是你的部门可能会收到很多电话。这些员工(处理器)一次只能处理一个电话,直到完成,与此同时,额外的电话必须排队。 在“鞋匠和精灵”的童话故事中,鞋匠有很多工作要做,当他睡着时,出现了一群精灵来为他制作鞋子。这里的工作是分布式的,但即使使用大量的物理处理器,在制造鞋子的某些部件时也会产生限制 - 例如,鞋底需要大量的时间去制作,这会限制制鞋的速度并改变你设计解决方案的方式。 因此,你尝试解决的问题驱动解决方案的设计。有一个迷人的抽象那就是将一个问题分解为子问题并且让它们独立运行,然后就是赤裸裸的现实。物理现实一次又一次地打了这种抽象的脸。 这只是问题的一部分。考虑一个制作蛋糕的工厂。我们不知何故在工人中分发了蛋糕制作任务,但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子,准备存放蛋糕。但是,在工人将蛋糕放入盒子之前,另一名工人投入并将蛋糕放入盒子中!我们的工人已经把蛋糕放进去了,然后就开始了!这两个蛋糕被砸碎并毁了。这是常见的“共享内存”问题,产生我们称之为竞争条件的问题,其结果取决于哪个工作人员可以首先将蛋糕放入盒子中(通常使用锁机制来解决问题,因此一个工作人员可以先拿到盒子并防止蛋糕被砸烂)。 当“同时”执行的任务相互干扰时,会出现问题。它可以以如此微妙和偶然的方式发生,可能公平地说,并发性“可以说是确定性的,但实际上是非确定性的。”也就是说,你可以假设编写通过维护和代码检查正常工作的并发程序。然而,在实践中,我们编写的并发程序似乎都能正常工作,但是在适当的条件下,将会失败。这些情况可能永远不会发生,或者在你在测试期间几乎很难发现它们。实际上,编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生,因此它们以客户投诉的形式出现。 这是学习并发中最强有力的论点之一:如果你忽略它,你可能会受伤。 因此,并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管Java 8在并发性方面做出了很大改进,但仍然没有像编译时验证(compile-time verification)或受检查的异常(checked exceptions)那样的安全网来告诉你何时出现错误。通过并发,你只能依靠自己,只有知识渊博,保持怀疑和积极进取的人,才能用Java编写可靠的并发代码。