天天看点

关于SICP中练习3.43的代码分析

在前一篇文章关于练习3.38的代码分析的基础上,完全可以用同样的套路来解决练习3.43。其实解决练习3.38的代码在通用性上还是蛮强的。比如练习3.40前面小问,只需要对 make-process 过程重写一下就好了,这里就不再废话了。

通过表来模拟并发确实是一个用起来很方便的技巧。同样,在解决练习3.43的后面几小问上也很有效。首先,我们大致的可以把此练习分成四个小问题:

1、如果这些进程是顺序运行的,那么经过任何次并发交换,这些账户里的余额还将是按照某种顺序排列的10、20和30。

2、请画出一个类似于图3-29中那样的时间图,说明如果采用本节第一版本的账户交换程序实现账户交换,那么这一条件就会被破坏。

3、在另一方面,也请论证,即使是使用这个exchange程序,在这些账户里的余额之和也仍然能得以保持不变。

4、请画出一个时序图,说明如果我们不做各个账户上交易的串行化,这一条件就可能被破坏。

第一小问我就不论述了,只要智商正常就都能做出来,我们从第二问开始。

首先还是上一篇文章中那个核心的过程 count-demo,因为源代码没有变,所以我在这里就不把代码放出来了。一切并发模拟的核心都是从它所运行的结果出发的。

然后是为了讨论问题的方便,我们将账户简单地抽象成了一个类,这里因为只是解决习题的模拟,纯粹以够用为主,所以就没有书中所写得那么复杂了,只要能满足基本要求就可以了。

; 模拟创建账户
(define (make-account name balance)
    (put 'balance name balance)
    (lambda (m)
        (cond ((eq? m 'set-balance!)
                (lambda (new) (put 'balance name new)))
            ((eq? m 'get-balance)
                (get 'balance name))
            (else (error "Unknown operation -- MAKE-ACCOUNT" m)))))

; 修改存款
(define (set-balance! acc new)
    ((acc 'set-balance!) new))
(define (get-balance acc)
    (acc 'get-balance))
           

从代码可见,这个类的本质其实只是在读写环境表中的一个存储单元而已。

然后是使用上面的简单账户类,写一些在模拟过程中会用到的便捷操作过程。

; 创建账户
(define a1 (make-account 'a1 10))
(define a2 (make-account 'a2 20))
(define a3 (make-account 'a3 30))

; 初始化三个账户的余额
(define (reset-accounts)
    (set-balance! a1 10)
    (set-balance! a2 20)
    (set-balance! a3 30))

; 获得三个账户的整体余额结果
(define (get-balances-all)
    (let ((a1 (get-balance a1))
          (a2 (get-balance a2))
          (a3 (get-balance a3)))
        (string->symbol
            (string-append
                "("
                (string a1)
                ")+("
                (string a2)
                ")+(" 
                (string a3)
                ")=("
                (string (+ a1 a2 a3))
                ")"))))
           

get-balances-all 这个过程其实是为了方便阅读模拟后所得到的结果而写的。因为在第三问中涉及到了余额的总和,所以索性就在结果里直接展示了三个账户余额求和的表达式。

然后就是对于书中第一个版本的 exchange 过程的模拟过程。首先可以将书中的 exchange 过程拆分成大概四个基本的过程:

read_acc1----------------对第一个账户的读取;

read_acc2----------------对第二个账户的读取;

write_acc1----------------对第一个账户的写入;

write_acc2----------------对第二个账户的写入;

然后是使用表的结构在并发的环境中对他们进行模拟,于是可以写成一个如下的类,思路和练习3.38中完全一致:

; 模拟交换过程
(define (make-exchange name acc1 acc2)
    ; 模拟账户1读取的过程
    (put name 'read_acc1
        (lambda ()          
            (put name 'acc1 (get-balance acc1))))
    
    ; 模拟账户2读取的过程
    (put name 'read_acc2
        (lambda ()
            (put name 'acc2 (get-balance acc2))))
    
    ; 模拟修改账户1的过程
    (put name 'write_acc1
        (lambda ()
            (set-balance!
                acc1
                (-  (get-balance acc1)
                    (-  (get name 'acc1)
                        (get name 'acc2))))))
    
    ; 模拟修改账户2的过程
    (put name 'write_acc2
        (lambda ()
            (set-balance!
                acc2
                (+  (get-balance acc2)
                    (-  (get name 'acc1)
                        (get name 'acc2))))))
    
    ; 返回特征表
    (list (cons name 'read_acc1) (cons name 'read_acc2) (cons name 'write_acc1) (cons name 'write_acc2)))

           

然后,就是为了能更方便地统计并发后的每个结果,再对每个模拟出来的结果抽象成一个类,也与做练习3.38中时候类似:

; 交错后可能性的抽象
(define (make-possibility demo)
    ; 得到过程
    (define (get-proc k-v)
        (get (car k-v) (cdr k-v)))

    ; 翻译单条操作信息
    (define (make-info k-v)
        (string-append
            (symbol->string (car k-v))
            "_"
            (symbol->string (cdr k-v))))
            
    ; 组合两条操作信息
    (define (add-info info1 info2)
        (string-append
            info1
            "->"
            info2))

    (let ((proc-list (map get-proc demo))
          (result '()))
        (reset-accounts)
        (for-each (lambda (proc) (proc)) proc-list)
        (set! result (get-balances-all))
        ; 分派函数
        (lambda (m)
            (cond ((eq? m 'result) result)
                ((eq? m 'info)
                    (accumulate add-info "\b\b\t" (map make-info demo)))
                (else (error "Unknown operation -- MAKE-RESULT" m))))))

           

这里就是要特别注意一点,在每一次执行模拟的结果后,需要对三个账户的余额进行初始值的复位,然后才能再进行下一次的继续模拟,也就是我们要用到刚刚上面所提到的便捷过程 reset-accounts.

接下来的 make-statistician 类在这里通用,与练习3.38中的一致,我就不放代码了,可以从上一篇文章中去找。但是,因为被模拟的过程的步骤增加了,所以并发结果的可能性自然也会增加。因此,在这个练习里,对之前的一个接口进行了一个升级,增加一个计数的功能进去,能更方便地统计出各个结果所出现的概率。

; 操作接口
(define (one-poss statistician result)
    (let ((count 0))
        (for-each
            (lambda (info)
                (newline)
                (display info)
                (set! count (+ 1 count)))
            ((statistician 'one) result))
        count))
           

接口 all-poss 则不变。

如此这般地修改之后,就可以完全按照解决练习3.38的套路取进行模拟了:

(define Peter (make-exchange 'Peter a1 a2))
(define Paul (make-exchange 'Paul a1 a3))

; 创建一个统计者
(define S (make-statistician (count-demo Peter Paul)))

           

而后就可以使用

(all-poss S)

语句进行结果的查看,我运行过后总共会出现三种结果:

(30)+(10)+(20)=(60)

(40)+(10)+(10)=(60)

(20)+(30)+(10)=(60)

从第二个结果中就可以回答问题2了,如果想要更进一步看看时序逻辑,就这样操作:

(one-poss S (string->symbol “(40)+(10)+(10)=(60)”))

它的运行结果会把出现这种情况的所有并发的可能性都打印到命令行上,并且返回这种可能性出现的次数。这样就直接省去了画图的麻烦。

然后我们观察到所有可能结果的余额总和都是60,这就回答了第三问。

最后我们可以用排列组合的思维去简单计算一下这样并发后样本空间的容量。也就是说两件都需要四步才能完成的事情,交错进行后,理论上一共可能会出现70种等概率的情况。可以执行下面代码来验证这一点:

(display
    (list
        (one-poss S (string->symbol "(30)+(10)+(20)=(60)"))
        (one-poss S (string->symbol "(40)+(10)+(10)=(60)"))
        (one-poss S (string->symbol "(20)+(30)+(10)=(60)"))))
           

执行后首先会在命令行上打印出所有的交错结果,最后会打印出三种可能性所分别占有的频数------(5 60 5)

然后,就剩第四小问了。其实很简单,如果不做账户交易的串行化,换句话也就是说允许在修改账户的时候也产生并发,那么只要将 make-exchange 重写一下,步骤再分得更细一点就可以了。

; 模拟交换过程
(define (make-exchange name acc1 acc2)
    ; 模拟账户1读取的过程
    (put name 'read_acc1
        (lambda ()          
            (put name 'acc1 (get-balance acc1))))
    
    ; 模拟账户2读取的过程
    (put name 'read_acc2
        (lambda ()
            (put name 'acc2 (get-balance acc2))))
    
    ; 模拟修改账户1的读取过程
    (put name 'write_r_acc1
        (lambda ()
            (put name 'write_r_d_acc1 (get-balance acc1))))
    
    ; 模拟修改账户1的写入过程
    (put name 'write_w_acc1
        (lambda ()
            (set-balance!
                acc1
                (-  (get name 'write_r_d_acc1)
                    (-  (get name 'acc1)
                        (get name 'acc2))))))
    
    ; 模拟修改账户2的读取过程
    (put name 'write_r_acc2
        (lambda ()
            (put name 'write_r_d_acc2 (get-balance acc2))))
    
    ; 模拟修改账户2的写入过程
    (put name 'write_w_acc2
        (lambda ()
            (set-balance!
                acc2
                (+  (get name 'write_r_d_acc2)
                    (-  (get name 'acc1)
                        (get name 'acc2))))))
    
    ; 返回特征表
    (list
        (cons name 'read_acc1)
        (cons name 'read_acc2)
        (cons name 'write_r_acc1)
        (cons name 'write_w_acc1)
        (cons name 'write_r_acc2)
        (cons name 'write_w_acc2)))
           

如此,运行之后的结果就变成为:

(30)+(10)+(20)=(60)

(30)+(10)+(10)=(50)

(20)+(10)+(10)=(40)

(40)+(10)+(10)=(60)

(20)+(30)+(10)=(60)

从结果二与三就可以论证第四问了。

还可以和上面一样,使用概率的角度去验证一下交错结果的能性,总共会存在924个等可能的交错结果,并且以上五种结果出现的频数各自为---------(28 200 200 468 28)

继续阅读