# 3.4.2控制并发的机制

> 译者： [https://sicp.comp.nus.edu.sg/chapters/65](https://sicp.comp.nus.edu.sg/chapters/65)

我们已经看到处理并发流程的困难根源在于需要考虑不同流程中事件顺序的交错。 例如，假设我们有两个过程，一个具有三个有序事件![%28a%2Cb%2Cc%29](img/588f8b66a6e858ef2f32c36535094ca3.jpg)，另一个具有三个有序事件![%28x%2Cy%2Cz%29](img/5e63018cdd7d6c576ab64f3049369a7b.jpg)。 如果两个进程同时运行，并且在执行方式上没有任何限制，那么事件的20种可能的排序与这两个进程的单独排序是一致的：![%20%5Cbegin%7Barray%7D%7Bcccc%7D%0A%20%20%20%20%20%20%28a%2Cb%2Cc%2Cx%2Cy%2Cz%29%20%26%20%28a%2Cx%2Cb%2Cy%2Cc%2Cz%29%20%26%20%28x%2Ca%2Cb%2Cc%2Cy%2Cz%29%20%26%20%28x%2Ca%2Cy%2Cz%2Cb%2Cc%29%5C%5C%0A%20%20%20%20%20%20%28a%2Cb%2Cx%2Cc%2Cy%2Cz%29%20%26%20%28a%2Cx%2Cb%2Cy%2Cz%2Cc%29%20%26%20%28x%2Ca%2Cb%2Cy%2Cc%2Cz%29%20%26%20%28x%2Cy%2Ca%2Cb%2Cc%2Cz%29%5C%5C%0A%20%20%20%20%20%20%28a%2Cb%2Cx%2Cy%2Cc%2Cz%29%20%26%20%28a%2Cx%2Cy%2Cb%2Cc%2Cz%29%20%26%20%28x%2Ca%2Cb%2Cy%2Cz%2Cc%29%20%26%20%28x%2Cy%2Ca%2Cb%2Cz%2Cc%29%5C%5C%0A%20%20%20%20%20%20%28a%2Cb%2Cx%2Cy%2Cz%2Cc%29%20%26%20%28a%2Cx%2Cy%2Cb%2Cz%2Cc%29%20%26%20%28x%2Ca%2Cy%2Cb%2Cc%2Cz%29%20%26%20%28x%2Cy%2Ca%2Cz%2Cb%2Cc%29%5C%5C%0A%20%20%20%20%20%20%28a%2Cx%2Cb%2Cc%2Cy%2Cz%29%20%26%20%28a%2Cx%2Cy%2Cz%2Cb%2Cc%29%20%26%20%28x%2Ca%2Cy%2Cb%2Cz%2Cc%29%20%26%20%28x%2Cy%2Cz%2Ca%2Cb%2Cc%29%0A%20%20%20%20%20%20%5Cend%7Barray%7D%20](img/ca39bca7c9e93a76b99bfad443800b5e.jpg)当程序员设计该系统时，我们将 必须考虑这20种排序中的每一种的影响，并检查每种行为是否可以接受。 随着过程和事件数量的增加，这种方法很快变得笨拙。

设计并发系统的一种更实用的方法是设计通用机制，使我们能够限制并发进程的交织，以便可以确保程序行为正确。 为此目的已经开发了许多机制。 在本节中，我们描述其中之一，_串行器_。

## 序列化对共享状态的访问

序列化实现以下思想：进程将同时执行，但是将存在某些不能同时执行的功能集合。 更准确地说，序列化创建了功能独特的集合，从而每个序列化集合中一次只能执行一次功能。 如果正在执行集合中的某个功能，则试图执行集合中任何功能的进程将被迫等待，直到第一次执行完成。

我们可以使用序列化来控制对共享变量的访问。 例如，如果我们要基于该变量的先前值更新共享变量，则可以在同一函数中访问对该变量的先前值并将新值分配给该变量。 然后，通过使用相同的序列化程序序列化所有这些函数，确保没有其他分配给该变量的函数可以与此函数同时运行。 这样可以保证在访问和相应的分配之间不能更改变量的值。

 <split>## JavaScript中的序列化器</split> 

为了使上述机制更加具体，假设我们已经扩展了JavaScript以包括一个名为`parallel_execute`的函数：`parallel_execute( f![_%7B1%7D](img/1b36a225f2c8754117f75bc4725c89aa.jpg), f![_%7B2%7D](img/c85b4cb21a8497c8dc9e23794dd63816.jpg), ![%5Cldots](img/9f57fa7487fb5cc87b777f74773ffb74.jpg), f![_%7Bk%7D](img/e003aedad154afde96a3cd3d87f14a78.jpg) )`

每个f必须是没有参数的函数。 函数`parallel_execute`为每个f创建一个单独的过程，该过程将f应用于（无参数）。 这些过程都同时运行。 [[1]](65#footnote-1)

作为如何使用此示例，请考虑

```js
let x = 10;

parallel_execute( () => { x = x * x; },
                  () => { x = x + 1; } );
```

这将创建两个并发进程-![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)（将`x`设置为`x`乘以`x`）和![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)，使`x`递增。 执行完成后，根据![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)和![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)事件的交织，`x`将留有五个可能值之一：

*   101：![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)将`x`设置为100，然后![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)将`x`递增到101。
*   121：![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)将`x`增加到11，然后![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)将`x`设置为`x`乘以`x`。
*   110：在评估`x * x`期间，![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)从![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)访问`x`值的两次之间，![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)从10变为11。
*   11：![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)访问`x`，然后![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)设置`x`为100，然后![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)设置`x`。
*   100：![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)（两次）访问`x`，然后![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg)将`x`设置为11，然后![P_1](img/85a3aae523949777146c6a5245104fa4.jpg)设置`x`。

我们可以通过使用由_序列化程序_创建的序列化函数来约束并发性。 串行器由`make_serializer`构造，其实现如下。 序列化器将一个函数作为参数，并返回一个行为与原始函数相似的序列化函数。 对给定序列化程序的所有调用都返回同一集中的序列化函数。

因此，与上面的示例相反，执行

```js
let x = 10;

const s = make_serializer();

parallel_execute(s( () => { x = x * x; }),
                 s( () => { x = x + 1; } ));
```

can produce only two possible values for `x`, 101 or 121. The other possibilities are eliminated, because the execution of ![P_1](img/85a3aae523949777146c6a5245104fa4.jpg) and ![P_2](img/74de7c930334b2fb24bb476a9b3d618f.jpg) cannot be interleaved.

这是 [3.1.1](49) 部分中`make_account`函数的版本，其中充值和提款已序列化：

```js
function make_account(balance) {
    function withdraw(amount) {
        if (balance > amount) {
            balance = balance - amount;
            return balance;
        } else {
            return "Insufficient funds";
        }
    }
    function deposit(amount) {
        balance = balance + amount;
        return balance;
    }
    const protector = make_serializer();
    function dispatch(m) {
        return m === "withdraw"
               ? protector(withdraw)
               : m === "deposit"
                 ? protector(deposit)
                 : m === "balance"
                   ? balance
                   : Error("Unknown request in make_account",
                           m);
    }
    return dispatch;
}
```

使用此实现，两个进程不能同时从单个帐户中撤出或存款到单个帐户中。 这样就消除了如图 [3.29](64#fig_3.29) 所示错误的源头，其中Peter在Paul访问余额以计算新值的时间与Paul实际执行分配的时间之间更改了帐户余额。 。 另一方面，每个帐户都有自己的序列化程序，因此可以同时进行不同帐户的存款和取款。

<exercise>Which of the five possibilities in the parallel execution shown above remain if we instead serialize execution as follows:

```js
let x = 10;

const s = make_serializer();

parallel_execute( () => { x = s( () => x * x ); }, 
                  s( () => { x = x + 1; } ) );
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Give all possible values of `x` that can result from executing

```js
let x = 10;

parallel_execute( () => { x = x * x; }, 
                  () => { x = x * x * x; } );
```

Which of these possibilities remain if we instead use serialized functions:

```js
let x = 10;

const s = make_serializer();

parallel_execute( s( () => x = x * x ) , 
                  s( () => x = x * x * x ) )
```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Ben Bitdiddle worries that it would be better to implement the bank account as follows (where the commented line has been changed):

```js
function make_account(balance) {
    function withdraw(amount) {
        if (balance > amount) {
            balance = balance - amount;
            return balance;
        } else {
            return "Insufficient funds";
        }
    }
    function deposit(amount) {
        balance = balance + amount;
        return balance;
    }
    const protected_ = make_serializer();
    function dispatch(m) {
        return m === "withdraw"
               ? protected_(withdraw)
                 : m === "deposit"
                   ? protected_(deposit)
                   : m === "balance"
                     ? protected_( () => balance )() // serialized
                     : error("Unknown request in make_account",
                             m);
                    }
                }
                return dispatch;
            }
```

because allowing unserialized access to the bank balance can result in anomalous behavior. Do you agree? Is there any scenario that demonstrates Ben's concern?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Ben Bitdiddle suggests that it's a waste of time to create a new serialized function in response to every `withdraw` and `deposit` message. He says that `make_account` could be changed so that the calls to `protected_` are done outside the `dispatch` function. That is, an account would return the same serialized function (which was created at the same time as the account) each time it is asked for a withdrawal function.

```js
function make_account(balance) {
    function withdraw(amount) {
        if (balance > amount) {
            balance = balance - amount;
            return balance;
        } else {
            return "Insufficient funds";
        }
    }
    function deposit(amount) {
        balance = balance + amount;
        return balance;
    }
    const protected_ = make_serializer();
    const protected_withdraw = protected_(withdraw);
    const protected_deposit = protected_(deposit);
    function dispatch(m) {
        return m === "withdraw"
               ? protected_withdraw
               : m === "deposit"
                 ? protected_deposit
                 : m === "balance"
                   ? balance
                   : error("Unknown request in make_account",
                           m);
    }
    return dispatch;
}
```

Is this a safe change to make? In particular, is there any difference in what concurrency is allowed by these two versions of `make_account` ?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

## 使用多个共享资源的复杂性

序列化程序提供了强大的抽象，可帮助隔离并发程序的复杂性，以便可以谨慎地（希望）正确地处理它们。 但是，尽管在只有一个共享资源（例如一个银行帐户）的情况下使用序列化程序相对简单，但是在有多个共享资源的情况下，并发编程会非常困难。

为了说明可能出现的困难之一，假设我们希望将余额交换到两个银行帐户中。 我们访问每个帐户以查找余额，计算余额之间的差额，从一个帐户中提取此差额，然后将其存入另一个帐户。 我们可以这样实现： [[2]](65#footnote-2)

```js
function exchange(account1, account2) {
    const difference = account1("balance") - account2("balance");
    account1("withdraw")(difference);
    account2("deposit")(difference);
}
```

当只有单个进程尝试进行交换时，此功能可以很好地工作。 但是，假设Peter和Paul都可以访问帐户![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)，![a_2](img/766453382c1cfc58875fc05901471a73.jpg)和![a_3](img/1ff9a43226ea821ae708836107ba3cfb.jpg)，并且Peter交换了![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)和![a_2](img/766453382c1cfc58875fc05901471a73.jpg)，而Paul同时交换了![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)和![a_3](img/1ff9a43226ea821ae708836107ba3cfb.jpg)。 即使为单个帐户序列化了帐户的存款和取款（如本节上面的`make_account`功能所示），`exchange`仍会产生错误的结果。 例如，Peter可以计算![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)和![a_2](img/766453382c1cfc58875fc05901471a73.jpg)余额的差额，但是Paul可以在Peter能够完成交换之前更改![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)中的余额。 [[3]](65#footnote-3) 为确保正确的行为，我们必须安排`exchange`功能在整个交换期间锁定对帐户的任何其他并发访问。

我们可以做到这一点的一种方法是使用两个帐户的序列化器来序列化整个`exchange`函数。 为此，我们将安排访问帐户的序列化程序。 请注意，我们通过公开序列化程序故意破坏了银行帐户对象的模块化。 以下版本的`make_account`与 [3.1.1](49) 部分中给出的原始版本相同，不同之处在于提供了一个序列化器来保护平衡变量，并且该序列化器通过 消息传递：

```js
function make_account_and_serializer(balance) {
    function withdraw(amount) {
        if (balance > amount) {
            balance = balance - amount;
        } else {
            "Insufficient funds";
        }
    }
    function deposit(amount) {
        balance = balance + amount;
        return balance;
    }
    const balance_serializer = make_serializer();
    function dispatch(m) {
        if (m === "withdraw") {
            return withdraw;
        } else if (m === "deposit") {
            return deposit;
        } else if (m === "balance") {
            return balance;
        } else if (m === "serializer") {
            return balance_serializer;
        } else {
            return "Unknown request - - MAKE-ACCOUNT";
        }
    }
    return dispatch;
}
```

我们可以使用它来进行序列化的存款和取款。 但是，与我们之前的序列化帐户不同，现在，银行帐户对象的每个用户都有责任明确管理序列化，例如： [[4]](65#footnote-4)

```js
function deposit(account, amount) {
    const s = account("serializer");
    const d = account("deposit");
    s(d(amount));
}
```

以这种方式导出序列化程序为我们提供了足够的灵活性来实施序列化的交换程序。 我们只需使用两个帐户的序列化器来序列化原始`exchange`函数：

```js
function serialized_exchange(account1, account2) {
    const serializer1 = account1("serializer");
    const serializer2 = account2("serializer");
    serializer1(serializer2(exchange))(account1, account2);
}
```

<exercise>Suppose that the balances in three accounts start out as <dollar></dollar>10, <dollar></dollar>20, and <dollar></dollar>30, and that multiple processes run, exchanging the balances in the accounts. Argue that if the processes are run sequentially, after any number of concurrent exchanges, the account balances should be <dollar></dollar>10, <dollar></dollar>20, and <dollar></dollar>30 in some order. Draw a timing diagram like the one in Figure <ref name="fig:bank-access">[3.29](64#fig_3.29)</ref> to show how this condition can be violated if the exchanges are implemented using the first version of the account-exchange program in this section. On the other hand, argue that even with this `exchange` program, the sum of the balances in the accounts will be preserved. Draw a timing diagram to show how even this condition would be violated if we did not serialize the transactions on individual accounts.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Consider the problem of transferring an amount from one account to another. Ben Bitdiddle claims that this can be accomplished with the following function, even if there are multiple people concurrently transferring money among multiple accounts, using any account mechanism that serializes deposit and withdrawal transactions, for example, the version of `make_account` in the text above.

```js
function transfer(from_account, to_account, amount) {
    from_account("withdraw")(amount);
    to_account("deposit")(amount);
}
```

Louis Reasoner claims that there is a problem here, and that we need to use a more sophisticated method, such as the one required for dealing with the exchange problem. Is Louis right? If not, what is the essential difference between the transfer problem and the exchange problem? (You should assume that the balance in `from_account` is at least `amount`.)<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Louis Reasoner thinks our bank-account system is unnecessarily complex and error-prone now that deposits and withdrawals aren't automatically serialized. He suggests that `make_account_and_serializer` should have exported the serializer (for use by such functions as `serialized_exchange`) in addition to (rather than instead of) using it to serialize accounts and deposits as `make_account` did. He proposes to redefine accounts as follows:

```js
function make_account_and_serializer(balance) {
    function withdraw(amount) {
        if (balance > amount) {
            balance = balance - amount;
        } else {
            "Insufficient funds";
        }
    }
    function deposit(amount) {
        balance = balance + amount;
        return balance;
    }
    const balance_serializer = make_serializer();
    function dispatch(m) {
        return m === "withdraw"
               ? balance_serializer(withdraw)
               : m === "deposit"
                 ? balance_serializer(deposit)
                 : m === "balance"
                   ? balance
                   : m === "serializer"
                     ? balance_serializer
                     : error("Unknown request in make_account",
                             m);
    }
    return dispatch;
}
```

Then deposits are handled as with the original `make_account`:

```js
function deposit(account, amount) {
    account("deposit")(amount);
}
```

Explain what is wrong with Louis's reasoning. In particular, consider what happens when `serialized_exchange` is called.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

## 实现序列化器

我们根据称为_互斥_的更原始的同步机制来实现串行器。 互斥锁是支持两种操作的对象-互斥锁可以是_获得的_，而互斥锁可以是_发布的_。 一旦获取了互斥锁，在释放互斥锁之前，不得继续对该互斥锁进行其他获取操作。 [[5]](65#footnote-5) 在我们的实现中，每个串行器都有一个关联的互斥体。 给定一个函数`p`，串行器将返回一个获取互斥量，运行`p`并释放互斥量的函数。 这确保了由序列化器产生的功能之一只能一次运行，这正是我们需要保证的序列化属性。

```js
function make_serializer() {
    const mutex = make_mutex();
    return p => {
        function serialized_p(args) {
            mutex("acquire");
            const val = p(args);
            mutex("release");
            return val;
        }
        return serialized_p;   
    };
}
```

互斥锁是一个可变对象（在这里，我们将使用一个元素列表，将其称为_单元格_），可以保留值true或false。 如果值为false，则可以获取互斥量。 当值为true时，互斥体不可用，任何尝试获取互斥体的进程都必须等待。

我们的互斥锁构造函数`make_mutex`首先将单元格内容初始化为false。 要获取互斥锁，我们测试单元格。 如果互斥锁可用，则将单元格内容设置为true并继续。 否则，我们将循环等待，尝试一次又一次地获取，直到发现互斥体可用为止。 [[6]](65#footnote-6) 要释放互斥锁，我们将单元格内容设置为false。

```js
function make_mutex() {
    const cell = list(false);
    function the_mutex(m) {
        return m === "aquire"
               ? ( test_and_set(cell)
                   ? the_mutex("aquire") // retry
                   : true )
               : m === "release"
                 ? clear(cell)
                 : error("Unknown request in mutex",
                         m);
    }
    return the_mutex;
}
function clear(cell) {
    set_head(cell, false);
}
```

函数`test_and_set`测试单元格并返回测试结果。 另外，如果测试为假，则`test_and_set`在返回假之前将单元格内容设置为真。 我们可以将此行为表示为以下功能：

```js
function test_and_set(cell) {
    if (head(cell)) {
        return true;
    } else {
        set_head(cell, true);
        return false;
    }
}
```

但是，`test_and_set`的这种实现方式目前并不足够。 这里有一个关键的微妙之处，这是并发控制进入系统的必要位置：`test_and_set`操作必须原子_进行_。 也就是说，我们必须保证，一旦某个进程测试了该单元并发现它为假，则在其他任何进程都可以测试该单元之前，该单元的内容实际上将被设置为true。 如果我们不做此保证，则互斥锁可能会以类似于图 [3.29](64#fig_3.29)[3.46](65#ex_3.46) 。）

`test_and_set`的实际实现取决于我们系统如何运行并发进程的细节。 例如，我们可能正在使用时间分割机制在顺序处理器上执行并发进程，该机制在各个进程之间循环，从而允许每个进程在中断之前运行很短的时间，然后继续进行下一个进程。 在这种情况下，`test_and_set`可以通过在测试和设置过程中禁用时间片来工作。 或者，多处理计算机提供直接在硬件中支持原子操作的指令。 [[7]](65#footnote-7)

<exercise>Suppose that we implement `test_and_set` using an ordinary function as shown in the text, without attempting to make the operation atomic. Draw a timing diagram like the one in figure <ref name="fig:bank-access">[3.29](64#fig_3.29)</ref> to demonstrate how the mutex implementation can fail by allowing two processes to acquire the mutex at the same time.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>A semaphore (of size ![n](img/493731e423d5db62086d0b8705dda0c8.jpg)) is a generalization of a mutex. Like a mutex, a semaphore supports acquire and release operations, but it is more general in that up to ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) processes can acquire it concurrently. Additional processes that attempt to acquire the semaphore must wait for release operations. Give implementations of semaphores

1.  就互斥而言
2.  就原子`test_and_set`操作而言。

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

## 僵局

现在，我们已经了解了如何实现序列化程序，我们可以看到即使使用上述`serialized_exchange`函数，帐户交换仍然存在问题。 想象一下，彼得尝试与![a_2](img/766453382c1cfc58875fc05901471a73.jpg)交换![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)，而保罗同时尝试与![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)交换![a_2](img/766453382c1cfc58875fc05901471a73.jpg)。 假设Peter的进程到达了进入保护![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)的序列化功能的地步，此后，Paul的进程进入了保护![a_2](img/766453382c1cfc58875fc05901471a73.jpg)的序列化功能。 现在，在Paul退出保护![a_2](img/766453382c1cfc58875fc05901471a73.jpg)的序列化功能之前，Peter无法继续（进入保护![a_2](img/766453382c1cfc58875fc05901471a73.jpg)的序列化功能）。 同样，Paul不能继续进行，直到Peter退出保护![a_1](img/7617c5f412d8ba08311c56a7e3032fcb.jpg)的序列化功能。 每个进程都永远停滞不前，等待另一个进程。 这种情况称为_死锁_。 在提供对多个共享资源的并发访问的系统中，死锁始终是一种危险。

在这种情况下，避免死锁的一种方法是为每个帐户赋予唯一的标识号并重写`serialized_exchange`，以便进程始终尝试首先输入保护编号最小的帐户的功能。 尽管此方法对于交换问题效果很好，但是在其他情况下，则需要更复杂的避免死锁技术，或者根本无法避免死锁的情况。 （请参阅练习 [3.48](65#ex_3.48)[3.49](65#ex_3.49) 。） [[8]](65#footnote-8)

<exercise>Explain in detail why the deadlock-avoidance method described above, (i.e., the accounts are numbered, and each process attempts to acquire the smaller-numbered account first) avoids deadlock in the exchange problem. Rewrite `serialized_exchange` to incorporate this idea. (You will also need to modify `make_account` so that each account is created with a number, which can be accessed by sending an appropriate message.)<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Give a scenario where the deadlock-avoidance mechanism described above does not work. (Hint: In the exchange problem, each process knows in advance which accounts it will need to get access to. Consider a situation where a process must get access to some shared resources before it can know which additional shared resources it will require.)<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_65_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

## 并发，时间和沟通

我们已经了解了对并发系统进行编程时，如何在不同进程访问共享状态时要求控制事件的顺序，以及如何通过明智地使用序列化程序来实现此控制。 但是并发性的问题要比这更深，因为从根本上看，&lt;quote&gt;共享状态的含义并不总是很清楚。&lt;/quote&gt;

诸如`test_and_set`之类的机制要求进程在任意时间检查全局共享标志。 这在现代高速处理器中是有问题且效率低下的，在现代高速处理器中，由于优化技术（例如流水线技术和缓存的内存），内存的内容可能并非在每个时刻都处于一致状态。 因此，在当代的多处理系统中，串行器范式已被新的并发控制方法所取代。 [[9]](65#footnote-9)

共享状态的问题还出现在大型分布式系统中。 例如，假设有一个分布式银行系统，其中各个分支银行维护银行余额的本地值，然后定期将其与其他分支机构所维护的值进行比较。 在这样的系统中，除非立即同步，否则将不确定帐户余额的&lt;quote&gt;值。 如果Peter将钱存入他与Paul共同持有的帐户中，那么我们何时应该说帐户余额已发生变化—当本地分支机构的余额发生变化时，还是直到同步之后才发生变化？ 并且，如果Paul从其他分支机构访问该帐户，那么对银行系统施加的合理约束是什么，以使行为&lt;quote&gt;是正确的&lt;/quote&gt;？ 唯一可能影响正确性的是Peter和Paul分别观察到的行为以及同步后立即显示帐户的&lt;quote&gt;状态&lt;/quote&gt;。 有关&lt;quote&gt;实际&lt;/quote&gt;帐户余额或同步之间事件顺序的问题可能无关紧要或毫无意义。 [[10]](65#footnote-10)&lt;/quote&gt;

这里的基本现象是，同步不同的流程，建立共享状态或对事件施加顺序要求流程之间进行通信。 本质上，并发控制中的任何时间概念都必须与通信紧密联系。 [[11]](65#footnote-11) 有趣的是，相对论中在时间与交流之间也出现了类似的联系，其中光速（可用于同步事件的最快信号）是一个基本常数，与 时间和空间。 实际上，我们在计算模型中处理时间和状态时遇到的复杂性实际上可能反映了物理宇宙的基本复杂性。

* * *

[[1]](65#footnote-link-1) <split>The function `parallel_execute` is not part of the JavaScript standard, but it can be implemented using the `SharedArrayBuffer` feature mentioned in section <ref name="sec:nature-of-time">[3.4.1](64)</ref>.</split>

[[2]](65#footnote-link-2) We have simplified `exchange` by exploiting the fact that our `deposit` message accepts negative amounts. (This is a serious bug in our banking system!)

[[3]](65#footnote-link-3) If the account balances start out as <dollar></dollar>10, <dollar></dollar>20, and <dollar></dollar>30, then after any number of concurrent exchanges, the balances should still be <dollar></dollar>10, <dollar></dollar>20, and <dollar></dollar>30 in some order. Serializing the deposits to individual accounts is not sufficient to guarantee this. See exercise <ref name="ex:exchange-bug">[3.43](65#ex_3.43)</ref>.

[[4]](65#footnote-link-4) Exercise <ref name="ex:export-serializer">[3.45](65#ex_3.45)</ref> investigates why deposits and withdrawals are no longer automatically serialized by the account.

[[5]](65#footnote-link-5) The term <quote>mutex</quote> is an abbreviation for _mutual exclusion_. The general problem of arranging a mechanism that permits concurrent processes to safely share resources is called the mutual exclusion problem. Our mutex is a simple variant of the _semaphore_ mechanism (see exercise <ref name="ex:semaphore">[3.47](65#ex_3.47)</ref>), which was introduced in the <quote>THE</quote> Multiprogramming System developed at the Technological University of Eindhoven and named for the university's initials in Dutch (<citation>Dijkstra 1968a</citation>). The acquire and release operations were originally called P and V, from the Dutch words _passeren_ (to pass) and _vrijgeven_ (to release), in reference to the semaphores used on railroad systems. Dijkstra's classic exposition (1968b) was one of the first to clearly present the issues of concurrency control, and showed how to use semaphores to handle a variety of concurrency problems.

[[6]](65#footnote-link-6) In most time-shared operating systems, processes that are blocked by a mutex do not waste time <quote>busy-waiting</quote> as above. Instead, the system schedules another process to run while the first is waiting, and the blocked process is awakened when the mutex becomes available.

[[7]](65#footnote-link-7) There are many variants of such instructions—including test-and-set, test-and-clear, swap, compare-and-exchange, load-reserve, and store-conditional—whose design must be carefully matched to the machine's processor–memory interface. One issue that arises here is to determine what happens if two processes attempt to acquire the same resource at exactly the same time by using such an instruction. This requires some mechanism for making a decision about which process gets control. Such a mechanism is called an _arbiter_. Arbiters usually boil down to some sort of hardware device. Unfortunately, it is possible to prove that one cannot physically construct a fair arbiter that works 100% of the time unless one allows the arbiter an arbitrarily long time to make its decision. The fundamental phenomenon here was originally observed by the fourteenth-century French philosopher Jean Buridan in his commentary on Aristotle's _De caelo_. Buridan argued that a perfectly rational dog placed between two equally attractive sources of food will starve to death, because it is incapable of deciding which to go to first.

[[8]](65#footnote-link-8) The general technique for avoiding deadlock by numbering the shared resources and acquiring them in order is due to Havender (1968). Situations where deadlock cannot be avoided require _deadlock-recovery_ methods, which entail having processes <quote>back out</quote> of the deadlocked state and try again. Deadlock-recovery mechanisms are widely used in database management systems, a topic that is treated in detail in <citation>Gray and Reuter 1993</citation>.

[[9]](65#footnote-link-9) One such alternative to serialization is called _barrier synchronization_. The programmer permits concurrent processes to execute as they please, but establishes certain synchronization points (<quote>barriers</quote>) through which no process can proceed until all the processes have reached the barrier. Modern processors provide machine instructions that permit programmers to establish synchronization points at places where consistency is required. The PowerPC![%5E%7B%5Ctextrm%7BTM%7D%7D](img/fe56a43a66550c6d751ed4dbf720bd67.jpg), for example, includes for this purpose two instructions called SYNC and EIEIO (Enforced In-order Execution of Input/Output).

[[10]](65#footnote-link-10) This may seem like a strange point of view, but there are systems that work this way. International charges to credit-card accounts, for example, are normally cleared on a per-country basis, and the charges made in different countries are periodically reconciled. Thus the account balance may be different in different countries.

[[11]](65#footnote-link-11) For distributed systems, this perspective was pursued by Lamport (1978), who showed how to use communication to establish <quote>global clocks</quote> that can be used to establish orderings on events in distributed systems.

