并行计算中的经典问题与挑战
在并行计算的世界里,存在着诸多经典问题,这些问题不仅在理论上具有重要意义,而且在实际的计算机系统中也有着广泛的应用。本文将深入探讨几个经典问题,包括生产者 - 消费者问题、读者 - 写者问题以及并行化的现实挑战,并介绍相关的解决方案和重要定律。
生产者 - 消费者问题
生产者 - 消费者问题是一个经典的同步问题。想象这样一个场景:Alice 和 Bob 离婚后,法官将宠物判给了 Alice,并让 Bob 负责喂养宠物。但宠物和 Alice 站在一边,看到 Bob 就会攻击他。因此,他们需要设计一个协议,让 Bob 能给宠物送食物,同时保证 Bob 和宠物不会同时出现在院子里。而且,这个协议不能浪费任何人的时间,即 Alice 只有在院子里有食物时才会放宠物出去,Bob 只有在宠物吃完所有食物后才会进入院子。
为了解决这个问题,他们采用了一个有趣的“罐子 - 绳子”协议。Bob 在 Alice 的窗台上放一个直立的罐子,将绳子的一端系在罐子上,另一端放在自己的客厅。当他把食物放进院子后,就拉绳子把罐子弄倒。此后,Alice 放宠物出去时会按以下步骤操作:
1. 等待罐子倒下。
2. 放出宠物。
3. 宠物回来后,检查它们是否吃完食物。如果吃完了,就把罐子重新立起来。
Bob 则按以下步骤操作:
1. 等待罐子立起。
2. 把食物放进院子。
3. 拉绳子把罐子弄倒。
这个协议满足以下三个重要属性:
-
互斥性
:Bob 和宠物永远不会同时在院子里。
-
无饥饿性
:如果 Bob 一直愿意喂食,宠物一直饥饿,那么宠物会无限次地吃到食物。
-
生产者 - 消费者属性
:宠物只有在有食物时才会进入院子,Bob 不会在有未吃完的食物时再提供更多食物。
下面我们来详细分析这个协议的这三个属性:
-
互斥性证明
:我们采用基于“状态机”的证明方法。把系着绳子的罐子看作一个状态机,它有立起和倒下两种状态,并在这两种状态之间不断转换。初始时,罐子要么立起要么倒下。假设罐子是倒下的,那么只有宠物可以进入院子,互斥性成立。要让罐子被 Alice 立起,宠物必须先离开院子,所以当罐子立起时,宠物不在院子里,并且在罐子再次被弄倒之前它们不会再进入,互斥性得以维持。要让罐子被弄倒,Bob 必须已经离开院子,并且在罐子再次立起之前他不会再进入,所以罐子被弄倒后互斥性也能维持。由于没有其他可能的状态转换,所以互斥性成立。
-
无饥饿性证明
:假设无饥饿性不成立,那么必然存在这样的情况:Alice 的宠物经常饥饿,院子里没有食物,而 Bob 试图提供食物却不成功。但罐子不可能是立起的,因为如果罐子立起,Bob 会提供食物并弄倒罐子,让宠物可以进食。所以罐子必然是倒下的,由于宠物饥饿,Alice 最终会把罐子立起来,这样就回到了前面的情况。
-
生产者 - 消费者属性证明
:互斥性属性意味着宠物和 Bob 永远不会同时在院子里。Bob 只有在 Alice 把罐子立起后才会进入院子,而 Alice 只有在没有食物时才会立起罐子。同样,宠物只有在 Bob 把罐子弄倒后才会进入院子,而 Bob 只有在放好食物后才会弄倒罐子。
然而,这个协议也存在等待的问题。如果 Bob 把食物放进院子后,马上度假去了,忘记把罐子立起来,那么即使有食物,宠物也可能会挨饿。在计算机科学中,生产者 - 消费者问题几乎出现在所有的并行和分布式系统中,它是处理器将数据放入通信缓冲区,以便通过网络互连或共享总线进行读取或传输的方式。
读者 - 写者问题
Bob 和 Alice 非常爱他们的宠物,所以他们需要交流关于宠物的简单信息。Bob 在他家门口立了一个广告牌,上面有一系列大瓷砖,每个瓷砖上有一个字母。Bob 有空时,会一次抬起一块瓷砖来张贴信息;Alice 有空时,会用望远镜一次看一块瓷砖来读取信息。
但这个系统存在问题。例如,Bob 张贴了“sell the cat”的信息,Alice 读到“sell the”时,Bob 把瓷砖取下并写了新信息“wash the dog”,结果 Alice 继续扫描时读到了“sell the dog”。
解决读者 - 写者问题有一些直接的方法:
- Alice 和 Bob 可以使用互斥协议,确保 Alice 只读取完整的句子,但她可能会错过一些句子。
- 他们也可以使用“罐子 - 绳子”协议,Bob 生成句子,Alice 消费句子。
既然这个问题这么容易解决,为什么还要提出来呢?互斥和生产者 - 消费者协议都需要等待,如果一方遇到意外延迟,另一方也会受到影响。在共享多处理器内存的环境中,解决读者 - 写者问题是让一个线程能够瞬间查看多个内存位置的一种方式。能够在不等待的情况下获取这样的视图,即不阻止其他线程在读取时修改这些位置,是一个强大的工具,可用于备份、调试等许多情况。令人惊讶的是,读者 - 写者问题确实有不需要等待的解决方案,后续我们会进一步探讨。
并行化的现实挑战
在理想情况下,从单处理器升级到 n 路多处理器应该能使计算能力提高约 n 倍。但在实际中,这几乎不可能实现。主要原因是,大多数现实世界的计算问题在并行化时,会产生处理器间通信和协调的成本。
以五个人粉刷五间房为例,如果所有房间大小相同,给每个人分配一间房,只要大家粉刷速度差不多,就能比一个人粉刷快五倍。但如果房间大小不同,比如有一间房是其他房间的两倍大,那么五个人粉刷就无法达到五倍的速度提升,因为整体完成时间取决于那间最大的房间。
为了分析这种情况,我们引入了 Amdahl 定律。该定律指出,任何复杂任务的加速比都受到必须顺序执行部分的限制。定义任务的加速比 S 为单处理器完成任务的时间(用时钟测量)与 n 个并发处理器完成相同任务的时间之比。假设单处理器完成任务的时间为 1,n 个并发处理器执行时,并行部分的时间为 p/n,顺序部分的时间为 1 - p,其中 p 是可以并行执行的任务比例。那么并行化计算的总时间为 1 - p + p/n,加速比 S 的计算公式为:
[S = \frac{1}{1 - p + \frac{p}{n}}]
我们通过房间粉刷的例子来说明 Amdahl 定律的影响。假设每间小房间为一个单位,大房间为两个单位。给每个房间分配一个粉刷工,意味着六个单位中有五个单位可以并行粉刷,即 p = 5/6,1 - p = 1/6。根据 Amdahl 定律,得到的加速比为:
[S = \frac{1}{1/6 + 1/6} = 3]
令人惊讶的是,五个人粉刷五间房,其中一间房是其他房间两倍大,只能实现三倍的加速比。如果有十间房和十个粉刷工,其中一间房是其他房间的两倍大,得到的加速比为:
[S = \frac{1}{1/11 + 1/11} = 5.5]
即使只有很小的不平衡,使用十个粉刷工也只能实现五倍的加速比,大约是我们天真预期的一半。
这表明,对于给定的问题和十处理器的机器,即使我们能将 90% 的解决方案并行化,但剩下的 10% 无法并行化,最终也只能实现五倍的加速比,而不是十倍。因此,努力从剩下的 10% 中挖掘尽可能多的并行性是值得的,尽管这通常很困难,因为这些额外的并行部分涉及大量的通信和协调。
在并行编程中,对于许多我们希望并行化的应用程序,有很大一部分可以很容易地确定为可以并行执行,因为它们不需要任何形式的协调或通信。但目前并没有确定这些部分的通用方法,这需要应用程序设计者根据对并行化算法的理解来判断。而更重要的问题是如何处理程序中剩下的部分,这些部分由于需要访问共享数据,并且需要进程间的协调和通信,因此很难并行化。
为了更好地理解并行编程中的这些问题,我们可以通过一些练习来巩固知识。以下是一些相关的练习题:
1.
哲学家就餐问题
:想象五个哲学家围坐在圆桌旁,桌上有一大盘米饭和五根筷子。每个哲学家思考一段时间后会感到饥饿,此时他会拿起离他最近的两根筷子吃饭,吃完后放下筷子继续思考。要求编写程序模拟哲学家的行为,确保不会出现两个哲学家同时拿一根筷子的情况,并且程序不会出现死锁和饥饿现象,还要为任意数量的哲学家提供无饥饿的解决方案。
2.
判断属性类型
:判断以下情况是安全属性还是活性属性,并指出感兴趣的好事情或坏事情。
- 顾客按到达顺序接受服务。
- 有升必有降。
- 如果两个或多个进程等待进入临界区,至少有一个会成功。
- 如果发生中断,一秒内会打印一条消息。
- 如果发生中断,会打印一条消息。
- 生活成本永不下降。
- 有两件事是确定的:死亡和纳税。
- 你总能认出哈佛人。
3.
改进生产者 - 消费者协议
:在生产者 - 消费者的故事中,假设 Bob 看不到 Alice 窗台上罐子的状态,设计一个使用罐子和绳子的生产者 - 消费者协议。
4.
囚犯策略问题
:一群囚犯被关押,监狱长设置了一个有开关的房间,开关状态为开或关且与其他设备无关。监狱长会随机选择囚犯进入房间,囚犯可以改变开关状态或保持不变。每个囚犯会任意多次进入房间。如果有囚犯宣称所有囚犯都至少进入过一次房间且宣称正确,他们将被释放;否则将被喂鳄鱼。分别设计在知道开关初始状态为关和不知道开关初始状态的情况下的获胜策略。
5.
囚犯猜帽子问题
:囚犯们站成一排,头上戴着红色或蓝色的帽子。每个囚犯不知道自己帽子的颜色,也不知道身后帽子的颜色,但能看到身前帽子的颜色。监狱长从队尾开始让每个囚犯猜自己帽子的颜色,只能回答“红色”或“蓝色”。回答正确的囚犯会被释放,错误的会被喂鳄鱼。囚犯们可以事先商量策略,设计一个能让至少 P - 1 个囚犯获释的策略。
6.
Amdahl 定律应用问题
:
- 假设一个程序的顺序部分占单处理器执行时间的 40%,求在多处理器机器上运行该程序的最大加速比。
- 假设顺序部分占程序计算时间的 30%,程序在 n 个进程上的加速比为 sn,要求将加速比提高一倍,即修订后的程序加速比 s′n > sn/2,需要将顺序部分替换为运行速度快 k 倍的版本,求 k 的值。
- 假设顺序部分可以加速三倍,修改后的程序在 n 个处理器上的运行时间是原来的一半,求顺序部分占总执行时间的比例(用 n 表示)。
7.
加速比推导问题
:已知应用程序在两个处理器上的加速比为 S2,使用 Amdahl 定律推导出在 n 个处理器上的加速比 Sn 关于 n 和 S2 的公式。
8.
处理器选择问题
:有两种选择,一种是购买一个每秒执行五万亿条指令的单处理器,另一种是购买一个十处理器的多处理器,每个处理器每秒执行一万亿条指令。使用 Amdahl 定律说明如何为特定应用选择合适的处理器。
通过这些问题的思考和解决,我们可以更深入地理解并行计算中的经典问题和 Amdahl 定律的应用,从而更好地进行并行编程,提高程序的性能。
并行计算中的经典问题与挑战
并行编程的应对策略与核心要点
在并行编程中,当我们面对那些难以并行化的部分时,需要掌握有效的工具和技术,以实现代码中需要协调和同步部分的高效编程,因为这些部分的改进可能会对性能产生深远影响。
以之前提到的打印素数的程序为例,代码的主要部分如下:
i = counter.getAndIncrement(); // 取未处理的下一个数字
if (isPrime(i))
print(i);
原本可以让线程以原子方式执行这三行代码,即放在一个互斥块中。但实际上,只有对
getAndIncrement()
的调用是原子的。从 Amdahl 定律的角度来看,这样做是合理的,因为要尽量减少顺序代码的粒度,也就是通过互斥访问的代码。而且,有效地实现互斥也很重要,因为围绕互斥共享计数器的通信和协调会对整个程序的性能产生重大影响。
并行编程的目标是让读者了解现代协调范式和并发数据结构背后的核心思想,呈现一个从基本原理到最佳实践工程技术的全面统一的图景。并行编程面临着诸多挑战,从宏观的智力问题到细微的工程技巧都有。我们采用逐步细化的方法来应对这些挑战,从一个理想化的模型开始,在这个模型中数学问题至关重要,然后逐渐转向更实际的模型,在这些模型中我们更多地关注基本工程原理。
例如,我们首先考虑的互斥问题是该领域最古老且最基本的问题之一。我们从数学角度分析各种算法在理想化架构上的可计算性和正确性属性。虽然这些算法本身对于现代架构并不实用,但学习如何对这些理想化算法进行推理是学习如何对更现实(也更复杂)的算法进行推理的必要步骤。特别是,学习如何对诸如饥饿和死锁等微妙的活性问题进行推理非常重要。
一旦我们掌握了如何对这些算法进行一般性的推理,就可以将注意力转向更现实的场景。我们会探索使用不同多处理器架构的各种算法和数据结构,以了解哪些是有效的以及为什么有效。
相关背景知识与总结
在解决这些并行计算问题的过程中,有一些背景知识值得了解。比如,关于 Alice 和 Bob 的故事大多改编自 Leslie Lamport 在 1984 年 ACM 分布式计算原理研讨会上的特邀演讲。读者 - 写者问题是一个经典的同步问题,在过去二十年里受到了众多论文的关注。Amdahl 定律则是由并行处理先驱 Gene Amdahl 提出的。
下面我们用一个表格来总结本文涉及的几个重要问题及其相关要点:
|问题名称|问题描述|解决方案|重要属性|
| ---- | ---- | ---- | ---- |
|生产者 - 消费者问题|Bob 给 Alice 的宠物送食物,需保证 Bob 和宠物不同时在院子,且不浪费时间|“罐子 - 绳子”协议|互斥性、无饥饿性、生产者 - 消费者属性|
|读者 - 写者问题|Bob 张贴信息,Alice 读取信息,避免读取不完整信息|互斥协议、“罐子 - 绳子”协议|可实现无等待读取|
|并行化挑战|从单处理器到多处理器升级难以实现理想加速比|遵循 Amdahl 定律挖掘并行性|加速比受顺序部分限制|
为了更直观地展示生产者 - 消费者协议的流程,我们用 mermaid 绘制一个流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{罐子状态}:::decision
B -->|倒下| C(Alice 等待):::process
C --> D(Alice 放出宠物):::process
D --> E{宠物是否吃完食物}:::decision
E -->|是| F(Alice 立起罐子):::process
E -->|否| C
B -->|立起| G(Bob 等待):::process
G --> H(Bob 放食物):::process
H --> I(Bob 弄倒罐子):::process
I --> B
综上所述,并行计算中的这些经典问题和挑战是相互关联的,解决它们需要综合运用各种知识和技术。通过深入理解这些问题,掌握相关的算法和定律,我们能够更好地进行并行编程,提高程序的性能和效率。同时,通过完成前面提到的练习题,我们可以进一步巩固所学知识,提升解决实际问题的能力。在未来的并行编程实践中,我们要不断探索和应用这些方法,以应对日益复杂的计算需求。
1337



被折叠的 条评论
为什么被折叠?



