@@ -113,3 +113,119 @@ HTTP/1.1 404 Not Found
113
113
一系列固定的响应代码是消息协议的普遍特性。协议的设计者试图预料通过协议发送的常用消息,并且赋为固定的代码来减少传送大小,以及建立通用的消息语义。在 HTTP 协议中,200 响应代码表示成功,而 404 表示资源没有找到的错误。其它大量[ 响应代码] ( http://en.wikipedia.org/wiki/List_of_HTTP_status_codes ) 也存在于 HTTP 1.1 标准中。
114
114
115
115
HTTP 是用于通信的固定格式,但是它允许传输任意的 Web 页面。其它互联网上的类似协议是 XMPP,即时消息的常用协议,以及 FTP,用于在客户端和服务器之间下载和上传文件的协议。
116
+
117
+ ## 4.3 并行计算
118
+
119
+ 计算机每一年都会变得越来越快。在 1965 年,英特尔联合创始人戈登·摩尔预测了计算机将如何随时间而变得越来越快。仅仅基于五个数据点,他推测,一个芯片中的晶体管数量每两年将翻一倍。近50年后,他的预测仍惊人地准确,现在称为摩尔定律。
120
+
121
+ 尽管速度在爆炸式增长,计算机还是无法跟上可用数据的规模。根据一些估计,基因测序技术的进步将使可用的基因序列数据比处理器变得更快的速度还要快。换句话说,对于遗传数据,计算机变得越来越不能处理每年需要处理的问题规模,即使计算机本身变得越来越快。
122
+
123
+ 为了规避对单个处理器速度的物理和机械约束,制造商正在转向另一种解决方案:多处理器。如果两个,或三个,或更多的处理器是可用的,那么许多程序可以更快地执行。当一个处理器在做一些计算的一个切面时,其他的可以在另一个切面工作。所有处理器都可以共享相同的数据,但工作并行执行。
124
+
125
+
126
+ 为了能够合作,多个处理器需要能够彼此共享信息。这通过使用共享内存环境来完成。该环境中的变量、对象和数据结构对所有的进程可见。处理器在计算中的作用是执行编程语言的求值和执行规则。在一个共享内存模型中,不同的进程可能执行不同的语句,但任何语句都会影响共享环境。
127
+
128
+ ### 4.3.1 共享状态的问题
129
+
130
+ 多个进程之间的共享状态具有单一进程环境没有的问题。要理解其原因,让我们看看下面的简单计算:
131
+
132
+ ``` py
133
+ x = 5
134
+ x = square(x)
135
+ x = x + 1
136
+ ```
137
+
138
+ ` x ` 的值是随时间变化的。起初它是 5,一段时间后它是 25,最后它是 26。在单一处理器的环境中,没有时间依赖性的问题。` x ` 的值在结束时总是 26。但是如果存在多个进程,就不能这样说了。假设我们并行执行了上面代码的最后两行:一个处理器执行` x = square(x) ` 而另一个执行` x = x + 1 ` 。每一个这些赋值语句都包含查找当前绑定到` x ` 的值,然后使用新值更新绑定。让我们假设` x ` 是共享的,同一时间只有一个进程读取或写入。即使如此,读和写的顺序可能会有所不同。例如,下面的例子显示了两个进程的每个进程的一系列步骤,` P1 ` 和` P2 ` 。每一步都是简要描述的求值过程的一部分,随时间从上到下执行:
139
+
140
+ ```
141
+ P1 P2
142
+ read x: 5
143
+ read x: 5
144
+ calculate 5*5: 25 calculate 5+1: 6
145
+ write 25 -> x
146
+ write x-> 6
147
+ ```
148
+
149
+
150
+ 在这个顺序中,` x ` 的最终值为 6。如果我们不协调这两个过程,我们可以得到另一个顺序的不同结果:
151
+
152
+ ```
153
+ P1 P2
154
+ read x: 5
155
+ read x: 5 calculate 5+1: 6
156
+ calculate 5*5: 25 write x->6
157
+ write 25 -> x
158
+ ```
159
+
160
+ 在这个顺序中,` x ` 将是 25。事实上存在多种可能性,这取决于进程执行代码行的顺序。` x ` 的最终值可能最终为 5,25,或预期值 26。
161
+
162
+ 前面的例子是无价值的。` square(x) ` 和` x = x + 1 ` 是简单快速的计算。我们强迫一条语句跑在另一条的后面,并不会失去太多的时间。但是什么样的情况下,并行化是必不可少的?这种情况的一个例子是银行业。在任何给定的时间,可能有成千上万的人想用他们的银行账户进行交易:他们可能想在商店刷卡,存入支票,转帐,或支付账单。即使一个帐户在同一时间也可能有活跃的多个交易。
163
+
164
+ 让我们看看第二章的` make_withdraw ` 函数,下面是修改过的版本,在更新余额之后打印而不是返回它。我们感兴趣的是这个函数将如何并发执行。
165
+
166
+ ``` py
167
+ >> > def make_withdraw (balance ):
168
+ def withdraw (amount ):
169
+ nonlocal balance
170
+ if amount > balance:
171
+ print (' Insufficient funds' )
172
+ else :
173
+ balance = balance - amount
174
+ print (balance)
175
+ return withdraw
176
+ ```
177
+
178
+ 现在想象一下,我们以 10 美元创建一个帐户,让我们想想,如果我们从帐户中提取太多的钱会发生什么。如果我们顺序执行这些交易,我们会收到资金不足的消息。
179
+
180
+ ``` py
181
+ >> > w = make_withdraw(10 )
182
+ >> > w(8 )
183
+ 2
184
+ >> > w(7 )
185
+ ' Insufficient funds'
186
+ ```
187
+
188
+ 但是,在并行中可以有许多不同的结果。下面展示了一种可能性:
189
+
190
+ ```
191
+ P1: w(8) P2: w(7)
192
+ read balance: 10
193
+ read amount: 8 read balance: 10
194
+ 8 > 10: False read amount: 7
195
+ if False 7 > 10: False
196
+ 10 - 8: 2 if False
197
+ write balance -> 2 10 - 7: 3
198
+ read balance: 2 write balance -> 3
199
+ print 2 read balance: 3
200
+ print 3
201
+ ```
202
+
203
+ 这个特殊的例子给出了一个不正确结果 3。就好像` w(8) ` 交易从来没有发生过。其他可能的结果是 2,和` 'Insufficient funds' ` 。这个问题的根源是:如果` P2 ` 在` P1 ` 写入值前读取余额,` P2 ` 的状态是不一致的(反之亦然)。` P2 ` 所读取的余额值是过时的,因为` P1 ` 打算改变它。` P2 ` 不知道,并且会用不一致的值覆盖它。
204
+
205
+ 这个例子表明,并行化的代码不像把代码行分给多个处理器来执行那样容易。变量读写的顺序相当重要。
206
+
207
+ 一个保证执行正确性的有吸引力的方式是,两个修改共享数据的程序不能同时执行。不幸的是,对于银行业这将意味着,一次只可以进行一个交易,因为所有的交易都修改共享数据。直观地说,我们明白,让 2 个不同的人同时进行完全独立的帐户交易应该没有问题。不知何故,这两个操作不互相干扰,但在同一帐户上的相同方式的同时操作就相互干扰。此外,当进程不读取或写入时,让它们同时运行就没有问题。
208
+
209
+ ### 4.3.2 并行计算的正确性
210
+
211
+ 并行计算环境中的正确性有两个标准。第一个是,结果应该总是相同。第二个是,结果应该和串行执行的结果一致。
212
+
213
+ 第一个条件表明,我们必须避免在前面的章节中所示的变化,其中在不同的方式下的交叉读写会产生不同的结果。例子中,我们从 10 美元的帐户取出了` w(8) ` 和` w(7) ` 。这个条件表明,我们必须始终返回相同的答案,独立于` P1 ` 和` P2 ` 的指令执行顺序。无论如何,我们必须以这样一种方式来编写我们的程序,无论他们如何相互交叉,他们应该总是产生同样的结果。
214
+
215
+ 第二个条件揭示了许多可能的结果中哪个是正确的。例子中,我们从 10 美元的帐户取出了` w(8) ` 和` w(7) ` ,这个条件表明结果必须总是余额不足,而不是 2 或者 3。
216
+
217
+ 当一个进程在程序的临界区影响另一个进程时,并行计算中就会出现问题。这些都是需要执行的代码部分,它们看似是单一的指令,但实际上由较小的语句组成。一个程序会以一系列原子硬件指令执行,由于处理器的设计,这些是不能被打断或分割为更小单元的指令。为了在并行的情况下表现正确,程序代码的临界区需要具有原子性,保证他们不会被任何其他代码中断。
218
+
219
+ 为了强制程序临界区在并发下的原子性,需要能够在重要的时刻将进程序列化或彼此同步。序列化意味着同一时间只运行一个进程 -- 这一瞬间就好像串行执行一样。同步有两种形式。首先是互斥,进程轮流访问一个变量。其次是条件同步,在满足条件(例如其他进程完成了它们的任务)之前进程一直等待,之后继续执行。这样,当一个程序即将进入临界区时,其他进程可以一直等待到它完成,然后安全地执行。
220
+
221
+ ### 4.3.3 保护共享状态:锁和信号量
222
+
223
+ 在本节中讨论的所有同步和序列化方法都使用相同的基本思想。它们在共享状态中将变量用作信号,所有过程都会理解并遵守它。这是一个相同的理念,允许分布式系统中的计算机协同工作 -- 它们通过传递消息相互协调,根据每一个参与者都理解和遵守的一个协议。
224
+
225
+ 这些机制不是为了保护共享状态而出现的物理障碍。相反,他们是建立相互理解的基础上。和出现在十字路口的各种方向的车辆能够安全通行一样,是同一种相互理解。这里没有物理的墙壁阻止汽车相撞,只有遵守规则,红色意味着“停止”,绿色意味着“通行”。同样,没有什么可以保护这些共享变量,除非当一个特定的信号表明轮到某个进程了,进程才会访问它们。
226
+
227
+ ** 锁。** 锁,也被称为互斥体(` mutex ` ),是共享对象,常用于发射共享状态被读取或修改的信号。不同的编程语言实现锁的方式不同,但是在 Python 中,一个进程可以调用` acquire() ` 方法来尝试获得锁的“所有权”,然后在使用完共享变量的时候调用` release() ` 释放它。当进程获得了一把锁,任何试图执行` acquire() ` 操作的其他进程都会自动等待到锁被释放。这样,同一时间只有一个进程可以获得一把锁。
228
+
229
+ 对于一把保护一组特定的变量的锁,所有的进程都需要编程来遵循一个规则:一个进程不拥有特定的锁就不能访问相应的变量。实际上,所有进程都需要在锁的` acquire() ` 和` release() ` 语句之间“包装”自己对共享变量的操作。
230
+
231
+ 我们可以把这个概念用于银行余额的例子中。该示例的临界区是从余额读取到写入的一组操作。我们看到,如果一个以上的进程同时执行这个区域,问题就会发生。为了保护临界区,我们需要使用一把锁。我们把这把锁称为` balance_lock ` (虽然我们可以命名为任何我们喜欢的名字)。为了锁定实际保护的部分,我们必须确保试图进入这部分时调用` acquire() ` 获取锁,以及之后调用` release() ` 释放锁,这样可以轮到别人。
0 commit comments