@@ -582,3 +582,107 @@ pending
582
582
```
583
583
584
584
首先,`read_to_many` 在`m` 上调用了`send(word)` 。这个协程正在等待循环中的`text = (yield )` ,之后打印出所发现的匹配,并且等待下一个`send` 。之后执行流返回到了`read_to_many` ,它向`p` 发送相同的行。所以,`text` 中的单词会按照顺序打印出来。
585
+
586
+ # # 4.3 并行计算
587
+
588
+ 计算机每一年都会变得越来越快。在 1965 年,在1965 ,英特尔联合创始人戈登·摩尔预测了计算机将如何随时间而变得越来越快。仅仅基于五个数据点,他推测,一个芯片中的晶体管数量每两年将翻一倍。近50 年后,他的预测仍惊人地准确,现在称为摩尔定律。
589
+
590
+ 尽管速度在爆炸式增长,计算机还是无法跟上可用数据的规模。根据一些估计,基因测序技术的进步将使可用的基因序列数据比处理器变得更快的速度还要快。换句话说,对于遗传数据,计算机变得越来越不能处理每年需要处理的问题规模,即使计算机本身变得越来越快。
591
+
592
+ 为了规避对单个处理器速度的物理和机械约束,制造商正在转向另一种解决方案:多处理器。如果两个,或三个,或更多的处理器是可用的,那么许多程序可以更快地执行。当一个处理器在做一些计算的一个切面时,其他的可以在另一个切面工作。所有处理器都可以共享相同的数据,但工作并行执行。
593
+
594
+
595
+ 为了能够合作,多个处理器需要能够彼此共享信息。这是=通过使用共享内存环境来完成。该环境中的变量、对象和数据结构对所有的进程可见。处理器在计算中的作用是执行编程语言的求值和执行规则。在一个共享内存模型中,不同的进程可能执行不同的语句,但任何语句都会影响共享环境。
596
+
597
+ # ## 4.3.1 共享状态的问题
598
+
599
+ 多个进程之间的共享状态具有单一进程环境没有的问题。要理解其原因,让我们看看下面的简单计算:
600
+
601
+ ```py
602
+ x = 5
603
+ x = square(x)
604
+ x = x + 1
605
+ ```
606
+
607
+ `x` 的值是随时间变化的。起初它是 5 ,一段时间后它是 25 ,最后它是 26 。在单一处理器的环境中,没有时间依赖性的问题。`x` 的值在结束时总是 26 。但是如果存在多个进程,就不能这样说了。假设我们并行执行了上面代码的最后两行:一个处理器执行`x = square(x)` 而另一个执行`x = x + 1 ` 。每一个这些赋值语句都包含查找当前绑定到`x` 的值,然后使用新值更新绑定。让我们假设`x` 是共享的,同一时间只有一个进程读取或写入。即使如此,读和写的顺序可能会有所不同。例如,下面的例子显示了两个进程的每个进程的一系列步骤,`P1` 和`P2` 。每一步都是简要描述的求值过程的一部分,随时间从上到下执行:
608
+
609
+ ```
610
+ P1 P2
611
+ read x: 5
612
+ read x: 5
613
+ calculate 5 * 5 : 25 calculate 5 + 1 : 6
614
+ write 25 -> x
615
+ write x-> 6
616
+ ```
617
+
618
+
619
+ 在这个顺序中,`x` 的最终值为 6 。如果我们不协调这两个过程,我们可以得到另一个顺序的不同结果:
620
+
621
+ ```
622
+ P1 P2
623
+ read x: 5
624
+ read x: 5 calculate 5 + 1 : 6
625
+ calculate 5 * 5 : 25 write x-> 6
626
+ write 25 -> x
627
+ ```
628
+
629
+ 在这个顺序中,`x` 将是 25 。事实上存在多种可能性,这取决于进程执行代码行的顺序。`x` 的最终值可能最终为 5 ,25 ,或预期值 26 。
630
+
631
+ 前面的例子是无价值的。`square(x)` 和`x = x + 1 ` 是简单快速的计算。我们强迫一条语句跑在另一条的后面,并不会失去太多的时间。但是什么样的情况下,并行化是必不可少的?这种情况的一个例子是银行业。在任何给定的时间,可能有成千上万的人想用他们的银行账户进行交易:他们可能想在商店刷卡,存入支票,转帐,或支付账单。即使一个帐户在同一时间也可能有活跃的多个交易。
632
+
633
+ 让我们看看第二章的`make_withdraw` 函数,下面是修改过的版本,在更新余额之后打印而不是返回它。我们感兴趣的是这个函数将如何并发执行。
634
+
635
+ ```py
636
+ >> > def make_withdraw(balance):
637
+ def withdraw(amount):
638
+ nonlocal balance
639
+ if amount > balance:
640
+ print (' Insufficient funds' )
641
+ else :
642
+ balance = balance - amount
643
+ print (balance)
644
+ return withdraw
645
+ ```
646
+
647
+ 现在想象一下,我们以 10 美元创建一个帐户,让我们想想,如果我们从帐户中提取太多的钱会发生什么。如果我们顺序执行这些交易,我们会收到资金不足的消息。
648
+
649
+ ```py
650
+ >> > w = make_withdraw(10 )
651
+ >> > w(8 )
652
+ 2
653
+ >> > w(7 )
654
+ ' Insufficient funds'
655
+ ```
656
+
657
+ 但是,在并行中可以有许多不同的结果。下面展示了一种可能性:
658
+
659
+ ```
660
+ P1: w(8 ) P2: w(7 )
661
+ read balance: 10
662
+ read amount: 8 read balance: 10
663
+ 8 > 10 : False read amount: 7
664
+ if False 7 > 10 : False
665
+ 10 - 8 : 2 if False
666
+ write balance -> 2 10 - 7 : 3
667
+ read balance: 2 write balance -> 3
668
+ print 2 read balance: 3
669
+ print 3
670
+ ```
671
+
672
+ 这个特殊的例子给出了一个不正确结果 3 。就好像`w(8 )` 交易从来没有发生过。其他可能的结果是 2 ,和`' Insufficient funds' ` 。这个问题的根源是:如果`P2` 在`P1` 写入值前读取余额,`P2` 的状态是不一致的(反之亦然)。`P2` 所读取的余额值是过时的,因为`P1` 打算改变它。`P2` 不知道,并且会用不一致的值覆盖它。
673
+
674
+ 这个例子表明,并行化的代码不像把代码行分给多个处理器来执行那样容易。变量读写的顺序相当重要。
675
+
676
+ 一个保证执行正确性的有吸引力的方式是,两个修改共享数据的程序不能同时执行。不幸的是,对于银行业这将意味着,一次只可以进行一个交易,因为所有的交易都修改共享数据。直观地说,我们明白,让 2 个不同的人同时进行完全独立的帐户交易应该没有问题。不知何故,这两个操作不互相干扰,但在同一帐户上的相同方式的同时操作就相互干扰。此外,当进程不读取或写入时,让它们同时运行就没有问题。
677
+
678
+ # ## 4.3.2 并行计算的正确性
679
+
680
+ 并行计算环境中的正确性有两个标准。第一个是,结果应该永远是相同的。第二个是,结果应该和串行执行的结果一致。
681
+
682
+ 第一个条件表明,我们必须避免在前面的章节中所示的变化,其中在不同的方式下的交叉读写会产生不同的结果。例子中,我们从 10 美元的帐户取出了`w(8 )` 和`w(7 )` 。这个条件表明,我们必须始终返回相同的答案,独立于`P1` 和`P2` 的指令执行顺序。无论如何,我们必须以这样一种方式来编写我们的程序,无论他们如何相互交叉,他们应该总是产生同样的结果。
683
+
684
+ 第二个条件揭示了许多可能的结果中哪个是正确的。例子中,我们从 10 美元的帐户取出了`w(8 )` 和`w(7 )` ,这个条件表明结果必须总是余额不足,而不是 2 或者 3 。
685
+
686
+ 当一个进程在程序的临界区影响另一个进程时,并行计算中就会出现问题。这些都是需要执行的代码部分,它们看似是单一的指令,但实际上由较小的语句组成。一个程序会以一系列原子硬件指令执行,由于处理器的设计,这些是不能被打断或分割为更小单元的指令。为了在并行的情况下表现正确,程序代码的临界区需要具有原子性,保证他们不会被任何其他代码中断。
687
+
688
+ 为了强制程序临界区在并发下的原子性,需要能够在重要的时刻将过程序列化或彼此同步。序列化意味着同一时间只运行一个进程 -- 这一瞬间就好像串行执行一样。同步有两种形式。首先是互斥,进程轮流访问一个变量。其次是条件同步,在满足条件(例如其他进程完成了它们的任务)之前进程一直等待,之后继续执行。这样,当一个程序即将进入临界区时,其他进程可以一直等待到它完成,然后安全地执行。
0 commit comments