并发:时间是一个本质问题

  1. 潜藏在状态、同一、变化后面的中心问题是,引入赋值之后,我们就必须承认时间在 所有的计算模形中的位置;
  2. 采用具有局部状态的计算对象建立模形,就会迫使我们去直面时间问题,并将它作为 程序设计中一个一个必不可少的概念;
  3. 将计算模形划分为一些能各自独立地并发演化的部分,常常也是很合适的,即使有关 的程序是在一台计算机上顺序执行,在实际写程序时就像它们将被并发地执行那样, 也能帮助程序员们避免那些并不必要的时间约束,因此也可能使程序更加模块化;
  4. 除了使程序更加模块化之外,并发计算还可能提供某种超越顺序计算的速度优势;
  5. 在出现了并发的情况下,由赋值引入的复杂性问题将变得更加严重,会在我们对时间 的理解中加入进一步的复杂性;

并发系统中时间的性质

  1. 对于并发的一种可能限制方式是规定,修改任意共享状态变量的两个操作都不允许 同时发生;
  2. 对于并发的另一种不那么严厉的限制方式是,保证并发系统产生出的结果与各个进 程按照 某种 方式顺序运行产生出的结果完全一样;

练习 3.38

1
2
;; 完全是体力活,列出各种排列组合...
;; 就在这里列举了。

控制并发的机制

  1. 在设计并发系统时,设法做出一些一般性的机制,使我们可能限制并行进程之间的 交错情况,以保证程序具有正确的行为方式;
  2. 串行化就是实现下面的想法:使进程可以并发地执行,但是其中也有一些过程不能 并发地执行,说的更准确些,串行化就是创建一些不同的过程集合,并且保证在每 个时刻,在任何一个串行化集合里至多只有一个过程的一个执行;
  3. 借助串行化去控制对共享变量的访问,举例说,如果我们希望基于某个共享变量已 有的值去更新它,那么就应该将访问这一变量的现有值和给这一变量赋新值的操作 都放入同一个过程里,而后设法保证,任何能给这个变量赋值的过程都不会与这个 过程并发运行,方法就是将所有这样的过程都放在同一个串行化集合里;
  4. 如果只存在一个共享资源,串行化的使用问题是相对比较简单的;如果存在着多项 共享资源并发程序设计就可能变得非常难以把握了;
  5. 使用一种更基本的称为 互斥元 的同步机制来实现串行化,互斥元是一种对象, 假定它提供了两个操作:获取(acquired) 和释放(release);
  6. 在那些提供了对于多种共享资源的开发访问的系统里,总是存在着死锁的危险;

练习 3.39-3.49

3.39

1
2
3
;; 121
;; 101
;; 100

3.40

1
2
3
4
5
6
7
8
9
;; without serialization
;; 1000000
;; 100
;; 1000
;; 10000
;; 100000

;; with serialization
;; 1000000

3.41

1
2
3
4
5
6
;; 并没有必要;
;; 第一,非串行地访问帐户过程并不会影响其他两个过程的结果;
;; 第二,可能存在一种担心,即非串行地访问帐户取到的值是不正确的,
;; 因为访问帐户可能处于其他并发过程的取值和修改值之间,
;; 访问帐户取到的值马上就无效了;
;; 这是不用担心的,因为可以这样说,访问帐户取值取的就是那一时刻的值。

3.42

1
2
;; 安全的;
;; 与原来的版本并无什么不同;

3.43

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
;; 顺序运行交换过程
;; 10 20 30
;; 10 30 20
;; 20 10 30
;; 20 30 10
;; 30 10 20
;; 30 20 10

;; 使用本节第一个版本的帐户交换程序
;; 10 20 30
;; 40 10 10
;; 20 20 20
;; 30 30 0
;; 10 40 10
;; 0 30 30
;; 30 0 30
;; 10 10 40
;; 20 10 30
;; 30 20 10
;; 10 30 20
;; 30 10 20
;; 20 30 10

3.44

1
2
3
4
5
6
7
8
;; Louis 的说法是错的;

;; 帐户余额交换过程中必须先计算帐户差额,
;; 而这个差额计算过程是可以与其他对这两个帐户的操作并行进行的,
;; 导致计算出来的差额可能是错误的,所以需要串行化整个 exchange 过程;

;; 而转移款项 transfer 过程中对帐户的所有操作都受到串行化组的保护;
;; 不必担心最初的 exchange 中出现的问题。

3.45

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
;; 当调用本节定义的 serialized-exchange 时,会出现死锁,

;; 串行化组里的并发过程运行 将卡在某个节点无法运行下去;
;; 比如,现在 exchange 过程在 serializer2 串行化组里;
;; 而 (serializer2 exchange) 过程在 serializer1 组里;
;; 当运行此 exchange 过程时,
;; (account 'withdraw) 与 (account 'deposit) 将返回两个被串行化的过程,
;; 分别属于串行化组 serializer1 和 serializer2,
;; exchange 过程内部的两个串行化过程必须等待同组的进程结束;
;; 而此时这两个组里都有过程作为进程在运行,
;; 即前面所说的 (serializer2 exchange) 和 exchange 本身;
;; 如此 exchange 将永远无法结束,这个调用将会死锁。

3.46

1
2
3
4
;; 如果没有企图使 test-and-set! 的操作原子化,
;; 两个并发进程就可以同时获取互斥元并进而运行下去,
;; 这是与以互斥元实现串行化的初衷相背的。
;; 画时序图是重复劳动,这里就不做了。

3.47

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;; a

(define (make-semaphore n)
  (let ((mutex (make-mutex))
	(original-n n))
    (define (acquire)
      (mutex 'acquire)
      (if (> n 0)
	  (begin (set! n (- n 1))
		 (mutex 'release)
		 'ok)
	  (begin (mutex 'release)
		 (acquire))))
    (define (release)
      (if (< n original-n)
	  (begin (mutex 'acquire)
		 (set! n (+ n 1))
		 (mutex 'release)
		 'ok)))
    (define (the-semaphore m)
      (cond ((eq? m 'acquire)
	     (acquire))
	    ((eq? m 'release)
	     (release))))
    the-semaphore))

;; b

(define (make-semaphore n)
  (let ((original-n n))
    (define (acquire)
      (if (test-and-set! n)
	  (acquire)
	  'ok))
    (define (release)
      (if (< n original-n)
	  (begin
	    (set! n (+ n 1))
	    'ok)))
    (define (the-semaphore m)
      (cond ((eq? m 'acquire)
	     (acquire))
	    ((eq? m 'release)
	     (release))))
    the-semaphore))

(define (test-and-set! n)
  (if (= n 0)
      true
      (begin (set! n (- n 1))
	     false)))

3.48

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(define (make-account-and-serializer balance n)
  (define (withdraw amount)
    (if (>= balance amount)
	(begin (set! balance (- balance amount))
	       balance)
	"Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((balance-serializer (make-serializer)))
    (define (dispatch m)
      (cond ((eq? m 'withdraw) withdraw)
	    ((eq? m 'deposit) deposit)
	    ((eq? m 'balance) balance)
	    ((eq? m 'serializer) balance-serializer)
	    ((eq? m 'label) n)
	    (else (error "Unknown request -- MAKE-ACCOUNT"
			 m))))
    dispatch))

(define (label acc)
  (acc 'label))

(define (serialized-exchange account1 account2)
  (let ((serializer1 (account1 'serializer))
	(serializer2 (account2 'serializer))
	(n1 (label account1))
	(n2 (label account2)))
    (if (> n1 n2)
	((serializer2 (serializer1 exchage))
	 account1
	 account2)
	((serializer1 (serializer2 exchage)))
	account1
	account2)))

3.49

1
2
3
4
5
6
7
8
9
;; 竟然无法理解自己之前写的回答了...
;; 摘录一条别人的情形描述:
;; https://sicp.readthedocs.io/en/latest/chp3/49.html

;; 关联帐号的其中一种状况产生的死锁无法使用 练习 3.48 的机制来防止。

;; 假设 peter 和 mary 是两夫妇,他们各自拥有自己的帐号 peter-acc 和 mary-acc ,并且这两个帐号都将对方的帐号设置成了关联帐号,也即是,当 peter-acc 的余额不足以支付的时候,它会去提取 mary-acc 的余额;而当 mary-acc 的余额不足以支付的时候,它也回去提取 peter-acc 的余额。

;; 现在,考虑这样一种情况, peter 和 mary 分别在不同的地方消费,然后各自账户的余额都不足以支付订单,于是 peter-acc 尝试访问关联帐号 mary-acc ,而 mary-acc 也在同一时间访问 peter-acc ,因为两个帐号都已经被打开,而且两个帐号都试图访问关联帐号,这样就造成了一个死锁:除非 peter 或 mary 的其中一个主动退出账户,否则支付永远都无法完成。