Why an expression using a global variable multiple times reads that variable multiple times instead of just once?

huangapple go评论59阅读模式
英文:

Why an expression using a global variable multiple times reads that variable multiple times instead of just once?

问题

以下是翻译好的内容:

SICP的第304页上,显示了以下代码片段:

(define x 10)

(parallel-execute (lambda () (set! x (* x x)))
                  (lambda () (set! x (+ x 1))))

关于这段代码,有以下说法:

x 最终会有五种可能的值之一

在这一点上,我感到有些困惑。我认为这两个进程只有在读取和写入 x 时(缺乏协调)才会相互干扰。因此,我本来预期以下几种情况,其中 a/A 是第一个进程对 x 的读/写操作,而 b/B 是第二个进程对 x 的读/写操作:

  • abAB -> 11
  • baAB -> 11
  • baBA -> 100
  • abBA -> 100
  • aAbB -> 101
  • bBaA -> 121

这总共有4种可能的结果。

但是这本书声称还有第5种情况,即110,这取决于第一个进程两次读取 x 的想法。在这种情况下,可能的情况是它第一次读取 10,然后第二个进程将11写入它,然后第二次读取11,从而将10乘以11,最终得到110。

为什么会这样?为什么第一个进程需要两次读取 x?如果它不需要两次读取,那么为什么语言设计成这样?这样做有什么优势?

此外,我很好奇这个问题是否适用于所有/某些/没有其他编程语言,例如 C++ 中是否也存在这个问题?

英文:

At page 304 from SICP, the following snippet is shown

(define x 10)

(parallel-execute (lambda () (set! x (* x x)))
                  (lambda () (set! x (+ x 1))))

about which it is said that
> x will be left with one of five possible values

At this point I was a bit perplexed. I thought that the two processes could interfere only in consequence of their (lack of) coordination in reading from and writing to x. So I'd have expected the following scenarii, where a/A is the first process' read/write of x and b/B is the second process' read/write of x,

  • abAB -> 11
  • baAB -> 11
  • baBA -> 100
  • abBA -> 100
  • aAbB -> 101
  • bBaA -> 121

which sum up to 4 possible results.

But the book claims there's a 5th one, 110, which hinges on the idea that the first process reads x twice, in which case a possible scenario is that it reads 10 the first time, before the second process writes 11 to it, and 11 the second time, afterwards, ending up multiplying 10 by 11, thus obtaining 110.

Why is that? Why does the first process need to read x twice? And if it doesn't need, then why was the language designed this way? What are the advantages in doing so?


By the way, I'm curious to know if this issue is common to all/some/none of the other languages, for instance, in C++.

答案1

得分: 2

一般情况下,我认为你应该假设代码按照它的编写方式进行评估,除非编程语言明确允许不这样做。对于具有并发性的程序,评估的每个步骤之间都可能发生任何事情。

这种假设导致了SICP规定的五种可能性。

实际上,即使对于没有并发性的程序,也会出现这个问题。例如,假设我写了这样的东西:

(define x 1)

(define (g f)
  (+ x (f x) x))

并假设评估器按照从左到右评估函数参数(Scheme没有指定这一点)。诱人的是可能会说这至少可以转换为

(define x 1)

(define (g f)
  (let ((v x))
    (+ v (f x) v)))

也许如果你知道+是它看起来的样子,你甚至可以进行更多的简化,可能会涉及浮点问题的代价。

但是这些都不是安全的,因为我可以这样说

(g (lambda (y) (set! x (* y y))))

当然,除非在阅读语言标准的繁琐细节时,您可能会发现评估器允许做出什么假设,以及不允许做出什么假设的复杂措辞。

但是如果你假设评估器确实执行它看起来要执行的操作,那么为确保程序行为正常所做的操作将始终偏向过于安全,而不是不够安全。这就是你想要做出的假设。回到SICP代码;如果你只希望有四种可能的结果,你可以将它更改为这样:

(define x 10)

(parallel-execute (lambda () (let ((y x)) (set! x (* y y))))
                  (lambda () (set! x (+ x 1)))))

而且你知道这是正确的,即使评估器可以对重复变量做出假设也是如此。

英文:

In general I think you should assume that code is evaluated as it is written, unless the language specifically allows it not to be. And for programs with concurrency anything could happen between any steps of the evaluation.

That assumption leads to the five possibilities specified by SICP.

In fact this problem happens even for programs without concurrency. For instance let's say I've written something like this:

(define x 1)

(define (g f)
  (+ x (f x) x))

And assume that the evaluator evaluates function arguments left-to-right (Scheme does not specify this). It's tempting to say that this could, at least, be turned into

(define x 1)

(define (g f)
  (let ((v x))
    (+ v (f x) v)))

And maybe if you know that + is what it looks like then you could even do some more simplification, possibly at the cost of floating-point problems.

But none of those are safe, because I can say

(g (lambda (y) (set! x (* y y))))

Except, of course, maybe when you read the gory details of the language standard you'll find complicated wording about what assumptions are allowed to be made by the evaluator, and what assumptions are not allowed to be made.

But if you assume that the evaluator does what it looks like it does then the things you do to ensure that the program behaves properly will always err on the side of being too safe, rather than not safe enough. And that's the assumption you want to make. To return to the SICP code; if you wanted there only to be four possible outcomes, you could change it to this:

(define x 10)

(parallel-execute (lambda () (let ((y x)) (set! x (* y y))))
                  (lambda () (set! x (+ x 1))))

And you know that this is right, even if it turns out that the evaluator is allowed to make assumptions about repeated variables.

答案2

得分: 1

x被读取两次,因为代码片段是这样编写的。你可以想象(set! x (* x x))的评估过程如下:

  1. 评估 *
  2. 评估 x(读取 x 的值)
  3. 评估 x(读取 x 的值)
  4. 调用 * 的值与 xx 的值
  5. x 的值设置为新值

这些步骤可以与第二个lambda的步骤交叉进行。如果第二个lambda中的set!在步骤2和3之间评估,x将具有新值。

你可以将代码片段更改为只读取x的值一次并使用两次,类似于以下方式:

(lambda ()
  (let ((temp x))
    (set! x (* temp temp))))
英文:

x is read twice, because the snippet is written this way. You can imagine that (set! x (* x x)) is evaluated like this:

  1. eval *
  2. eval x ("read" value of x)
  3. eval x ("read" value of x)
  4. call value of * to values of x and x
  5. set! value of x to new value

These steps can be interleaved with steps from the second lambda. If set! from the second lambda is evaluated between steps 2 and 3, x will have a new value.

You could change that snippet to read the value of x only once and use it twice, something like:

(lambda ()
  (let ((temp x))
    (set! x (* temp temp))))

huangapple
  • 本文由 发表于 2023年5月13日 22:41:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76243284.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定