We've seen that the difficulty in dealing with concurrent
threads
is rooted in the need to consider the interleaving of the order of events
in the different
threads.
For example, suppose we have two
threads,
one with three ordered events $(a,b,c)$
and one with three ordered events $(x,y,z)$.
If the two
threads
run concurrently, with no constraints on how their execution is
interleaved, then there are 20 different possible orderings for the events
that are consistent with the individual orderings for the two
threads:
\[ \begin{array}{cccc}
(a,b,c,x,y,z) & (a,x,b,y,c,z) & (x,a,b,c,y,z) & (x,a,y,z,b,c)\\
(a,b,x,c,y,z) & (a,x,b,y,z,c) & (x,a,b,y,c,z) & (x,y,a,b,c,z)\\
(a,b,x,y,c,z) & (a,x,y,b,c,z) & (x,a,b,y,z,c) & (x,y,a,b,z,c)\\
(a,b,x,y,z,c) & (a,x,y,b,z,c) & (x,a,y,b,c,z) & (x,y,a,z,b,c)\\
(a,x,b,c,y,z) & (a,x,y,z,b,c) & (x,a,y,b,z,c) & (x,y,z,a,b,c)
\end{array} \]
As programmers designing this system, we would have to consider the
effects of each of these 20 orderings and check that each behavior is
acceptable. Such an approach rapidly becomes unwieldy as the numbers of
threads and events increase.
A more practical approach to the design of concurrent systems is to
devise general mechanisms that allow us to constrain the interleaving
of concurrent
threads
so that we can be sure that the program
behavior is correct. Many mechanisms have been developed for this
purpose. In this section, we describe one of them, the
serializer.
Serialization implements the following idea:
Threads
will execute concurrently, but there will be certain collections of
functions
that cannot be executed concurrently. More precisely, serialization
creates distinguished sets of
functions
such that only one execution of a
function
in each serialized set is permitted to happen at a time. If some
function
in the set is being executed, then a
thread
that attempts to execute any
function
in the set will be forced to wait
until the first execution has finished.
We can use serialization to control access to shared variables.
For example, if we want to update a shared variable based on the
previous value of that variable, we put the access to the previous
value of the variable and the assignment of the new value to the
variable in the same
function.
We then ensure that no other
function
that assigns to the variable can run concurrently with this
function
by serializing all of these
functions
with the same serializer. This guarantees that the value of the
variable cannot be changed between an access and the corresponding
assignment.
Each $f$ must be a function of no arguments.
The function concurrent_execute
creates a separate thread for each
$f$, which applies
$f$ (to no arguments).
These
threads
all run concurrently.
let x = 10;
concurrent_execute(() => { x = x * x; },
() => { x = x + 1; });
This creates two concurrent
threads—$T_1$, which sets
x to x times
x, and $T_2$,
which increments x. After execution is
complete, x will be left with one of five
possible values, depending on the interleaving of the events of
$T_1$ and $T_2$:
101:
$T_1$
sets x to 100 and then
$T_2$ increments
x to 101.
121:
$T_2$ increments
x to 11 and then
$T_1$ sets x
to x times
x.
110:
$T_2$ changes
x from 10 to 11 between the two
times that $T_1$
accesses the value of
x during the evaluation of
x * x.
11:
$T_2$ accesses
x, then
$T_1$ sets x
to 100,
then $T_2$ sets
x.
100:
$T_1$ accesses
x (twice),
then $T_2$ sets
x to 11,
then $T_1$ sets
x.
We can constrain the concurrency by using serialized
functions,
which are created by serializers. Serializers are constructed by
make_serializer,
whose implementation is given below. A serializer takes a
function
as argument and returns a serialized
function
that behaves like the original
function.
All calls to a given serializer return serialized
functions
in the same set.
let x = 10;
const s = make_serializer();
concurrent_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
$T_1$ and
$T_2$ cannot be interleaved.
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 protect = make_serializer();
function dispatch(m) {
return m === "withdraw"
? protect(withdraw)
: m === "deposit"
? protect(deposit)
: m === "balance"
? balance
: error(m, "unknown request -- make_account");
}
return dispatch;
}
With this implementation, two
threads
cannot be withdrawing from or
depositing into a single account concurrently. This eliminates the source
of the error illustrated in figure 3.53,
where Peter changes the account balance between the times when Paul accesses
the balance to compute the new value and when Paul actually performs the
assignment. On the other hand, each account has its own serializer,
so that deposits and withdrawals for different accounts can proceed
concurrently.
let x = 10;
const s = make_serializer();
concurrent_execute( () => { x = s(() => x * x)(); },
s(() => { x = x + 1; }));
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.
Give all possible values of x
that can result from executing
次を実行した結果として得られるxの可能な値をすべて挙げてください。
let x = 10;
concurrent_execute(() => { x = x * x; },
() => { x = x * x * x; });
Which of these possibilities remain if we instead use serialized
functions:
代わりにシリアル化された関数を使った場合、これらの可能性のうちどれが残るでしょうか。
let x = 10;
const s = make_serializer();
concurrent_execute(s(() => { x = x * x; }),
s(() => { x = x * x * x; }));
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.
Ben Bitdiddle worries that it would be better to implement the bank
account as follows (where the commented line has been changed):
Ben Bitdiddleは、銀行口座を次のように実装した方がよいのではないかと心配しています(コメントの付いた行が変更されています)。
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 protect = make_serializer();
function dispatch(m) {
return m === "withdraw"
? protect(withdraw)
: m === "deposit"
? protect(deposit)
: m === "balance"
? protect(() => balance)(undefined) // serialized
: error(m, "unknown request -- make_account");
}
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?
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.
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
protect
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.
Ben Bitdiddleは、withdrawとdepositのメッセージのたびに新しいシリアル化された関数を作るのは時間の無駄だと指摘しています。彼は、make_accountを変更して、protectの呼び出しをdispatch関数の外で行えるようにできるはずだと言っています。つまり、口座は引き出し関数を要求されるたびに、(口座と同時に作られた)同じシリアル化された関数を返すというわけです。
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 protect = make_serializer();
const protect_withdraw = protect(withdraw);
const protect_deposit = protect(deposit);
function dispatch(m) {
return m === "withdraw"
? protect_withdraw
: m === "deposit"
? protect_deposit
: m === "balance"
? balance
: error(m, "unknown request -- make_account");
}
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
?
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.
Complexity of using multiple shared resources
複数の共有リソースを使うことの複雑さ
Serializers provide a powerful abstraction that helps isolate the
complexities of concurrent programs so that they can be dealt with
carefully and (hopefully) correctly. However, while using serializers
is relatively straightforward when there is only a single shared
resource (such as a single bank account), concurrent programming can
be treacherously difficult when there are multiple shared resources.
To illustrate one of the difficulties that can arise, suppose we wish to
swap the balances in two bank accounts. We access each account to find
the balance, compute the difference between the balances, withdraw this
difference from one account, and deposit it in the other account.
We could implement this as
follows:
This
function
works well when only a single
thread
is trying to do the exchange. Suppose, however, that Peter and Paul both
have access to accounts $a_1$,
$a_2$, and $a_3$, and
that Peter exchanges $a_1$ and
$a_2$ while Paul concurrently exchanges
$a_1$ and $a_3$.
Even with account deposits and withdrawals
serialized for individual accounts (as in the
make_account
function
shown above in this section), exchange can
still produce incorrect results. For example, Peter might compute the
difference in the balances for $a_1$ and
$a_2$, but then Paul might change the balance in
$a_1$ before Peter is able to complete the
exchange.
この関数は、交換を行おうとするスレッドが1つだけの場合はうまく動作します。しかし、Peter と Paul の両方が口座$a_1$、$a_2$、$a_3$にアクセスでき、Peter が$a_1$と$a_2$を交換するのと並行して、Paul が$a_1$と$a_3$を交換するとしましょう。本節で前に示したmake_account関数のように、口座ごとに預け入れと引き出しがシリアル化されていたとしても、exchangeは依然として誤った結果を生み出す可能性があります。たとえば、Peter が$a_1$と$a_2$の残高の差を計算した後、Peter が交換を完了する前に、Paul が$a_1$の残高を変更してしまうかもしれません。
For correct behavior, we must arrange for the
exchange
function
to lock out any other concurrent accesses to the accounts during the
entire time of the exchange.
One way we can accomplish this is by using both accounts' serializers
to serialize the entire exchange
function.
To do this, we will arrange for access to an account's serializer.
Note that we are deliberately breaking the modularity of the bank-account
object by exposing the serializer. The following version of
make_account
is identical to the original version given in
section 3.1.1, except that a
serializer is provided to protect the balance variable, and the serializer
is exported via message passing:
function make_account_and_serializer(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 balance_serializer = make_serializer();
return m => m === "withdraw"
? withdraw
: m === "deposit"
? deposit
: m === "balance"
? balance
: m === "serializer"
? balance_serializer
: error(m, "unknown request -- make_account");
}
We can use this to do serialized deposits and withdrawals. However,
unlike our earlier serialized account, it is now the responsibility of
each user of bank-account objects to explicitly manage the
serialization, for example as
follows:
function deposit(account, amount) {
const s = account("serializer");
const d = account("deposit");
s(d(amount));
}
Exporting the serializer in this way gives us enough flexibility to
implement a serialized exchange program. We simply serialize the original
exchange
function
with the serializers for both accounts:
Suppose that the balances in three accounts start out as $10,
$20, and $30, and that multiple
threads
run, exchanging the balances in the accounts. Argue that if the
threads
are run sequentially,
after any number of concurrent exchanges, the account balances should be
$10, $20, and $30 in some order.
Draw a timing diagram like the one in
figure 3.53 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.
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.
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.
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.)
Louis Reasoner は、ここには問題があり、交換問題に対処するために必要だったような、もっと洗練された方法を使う必要があると主張しています。Louis
は正しいでしょうか。もし正しくないなら、振替問題と交換問題の本質的な違いは何でしょうか。(ただし
from_account
の残高は少なくとも amount 以上であると仮定して構いません。)
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.
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:
Louis Reasoner は、預け入れと引き出しが自動的にシリアル化されなくなったため、私たちの銀行口座システムは不必要に複雑でエラーを起こしやすいと考えています。彼は、
make_account_and_serializer
が、
make_account
がやっていたように口座と預け入れをシリアル化するためにシリアライザを使うのに加えて(その代わりにではなく)、シリアライザを
(serialized_exchange のような関数から利用できるように)
エクスポートすべきだったと提案しています。彼は次のように口座を再定義することを提案します。
function make_account_and_serializer(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 balance_serializer = make_serializer();
return m => m === "withdraw"
? balance_serializer(withdraw)
: m === "deposit"
? balance_serializer(deposit)
: m === "balance"
? balance
: m === "serializer"
? balance_serializer
: error(m, "unknown request -- make_account");
}
Then deposits are handled as with the original
make_account:
そして預け入れは元の
make_account と同じように扱います。
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.
Louis の論理のどこが間違っているか説明してください。特に、
serialized_exchange
が呼び出されたときに何が起こるかを考えてみてください。
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.
Implementing serializers
シリアライザの実装
We implement serializers in terms of a more primitive synchronization
mechanism called a
mutex. A mutex is an object that supports two
operations—the mutex can be
acquired, and the mutex can be
released. Once a mutex has been acquired, no other acquire
operations on that mutex may proceed until the mutex is
released.
In our implementation, each serializer has an associated mutex. Given a
function
f,
the serializer returns a
function
that acquires the mutex, runs
f,
and then releases the mutex. This ensures that only one of the
functions
produced by the serializer can be running at once, which is
precisely the serialization property that we need to guarantee.
今回の実装では、各シリアライザは1つのミューテックスを持ちます。
関数
f
が与えられると、シリアライザは、ミューテックスを取得し、
f を実行し、
その後ミューテックスを解放する
関数
を返します。これによって、シリアライザが生成した
関数
のうち同時に実行できるのは一つだけであることが保証されます。これはまさに、私たちが保証する必要のあるシリアル化の性質です。
To apply serializers to functions that take an arbitrary number of arguments,
we use JavaScript's rest parameter and spread syntax.
The ... in front of the
parameter args collects
the rest (here all) of the arguments of any call of the function
into a vector data structure.
The
... in front of
args in
the application
f(...args)
spreads the elements of
args so that
they become separate arguments of
f.
function make_serializer() {
const mutex = make_mutex();
return f => {
function serialized_f(...args) {
mutex("acquire");
const val = f(...args);
mutex("release");
return val;
}
return serialized_f;
};
}
The mutex is a mutable object (here we'll use a one-element list,
which we'll refer to as a
cell) that can hold the value true or false. When the value is
false, the mutex is available to be acquired. When the value is true, the
mutex is unavailable, and any
thread
that attempts to acquire the mutex must wait.
Our mutex constructor
make_mutex
begins by initializing the cell contents to false. To acquire the mutex,
we test the cell. If the mutex is available, we set the cell contents to
true and proceed. Otherwise, we wait in a loop, attempting to acquire over
and over again, until we find that the mutex is available.
To release the mutex, we set the cell contents to false.
ミューテックスを解放するには、セルの内容を false に設定します。
function make_mutex() {
const cell = list(false);
function the_mutex(m) {
return m === "acquire"
? test_and_set(cell)
? the_mutex("acquire") // retry
: true
: m === "release"
? clear(cell)
: error(m, "unknown request -- mutex");
}
return the_mutex;
}
function clear(cell) {
set_head(cell, false);
}
The function test_and_set
tests the cell and returns the result of the test. In addition, if the
test was false,
test_and_set
sets the cell contents to true before returning false. We can express this
behavior as the following
function:
function test_and_set(cell) {
if (head(cell)) {
return true;
} else {
set_head(cell, true);
return false;
}
}
However, this implementation of
test_and_set
does not suffice as it stands. There is a crucial subtlety here, which is
the essential place where concurrency control enters the system: The
test_and_set
operation must be performed
atomically. That is, we must guarantee that, once a
thread
has tested the cell and found it to be false, the cell contents will
actually be set to true before any other
thread
can test the cell. If we do not make this guarantee, then the mutex can
fail in a way similar to the bank-account failure in
figure 3.53. (See
exercise 3.46.)
The actual implementation of
test_and_set
depends on the details of how our system runs concurrent
threads.
For example, we might be executing concurrent
threads
on a sequential processor using a
time-slicing mechanism that cycles through the
threads,
permitting each
thread
to run for a short time before interrupting it
and moving on to the next
thread.
In that case,
test_and_set
can work by disabling time slicing during the testing and
setting.
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 3.53 to demonstrate how the mutex
implementation can fail by allowing two
threads
to acquire the mutex at the same time.
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.
A semaphore
(of size $n$) 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$
threads
can acquire it
concurrently. Additional
threads
that attempt to acquire the semaphore must wait for release operations.
Give implementations of semaphores
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.
Deadlock
デッドロック
Now that we have seen how to implement serializers, we can see
that account exchanging still has a problem, even with the
serialized_exchange
function
above.
Imagine that Peter attempts to exchange $a_1$
with $a_2$ while Paul concurrently attempts to
exchange $a_2$ with
$a_1$. Suppose that Peter's
thread
reaches the point where it has entered a serialized
function
protecting $a_1$ and, just after that,
Paul's
thread
enters a serialized
function
protecting $a_2$. Now Peter cannot proceed (to
enter a serialized
function
protecting $a_2$) until Paul exits the serialized
function
protecting $a_2$. Similarly, Paul cannot proceed
until Peter exits the serialized
function
protecting $a_1$. Each
thread
is stalled forever, waiting for the other. This situation is called a
deadlock. Deadlock is always a danger in systems that provide
concurrent access to multiple shared resources.
One way to avoid the
deadlock in this situation is to give each account a
unique identification number and rewrite
serialized_exchange
so that a
thread
will always attempt to enter a
function
protecting the lowest-numbered account first. Although this method works
well for the exchange problem, there are other situations that require more
sophisticated deadlock-avoidance techniques, or where deadlock cannot
be avoided at all. (See exercises 3.48
and 3.49.)
Explain in detail why the
deadlock-avoidance method described above,
(i.e., the accounts are numbered, and each
thread
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.)
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.
Give a scenario where the deadlock-avoidance mechanism described
above does not work. (Hint: In the exchange problem, each
thread
knows in advance which accounts it will need to get access to. Consider a
situation where a
thread
must get access to some shared resources before it can know which additional
shared resources it will require.)
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.
Concurrency, time, and communication
並行性、時間、通信
We've seen how programming concurrent systems requires controlling
the ordering of events when different
threads
access shared state, and we've seen how to achieve this control
through judicious use of serializers. But the problems of concurrency
lie deeper than this, because, from a fundamental point of view, it's
not always clear what is meant by shared state.
Mechanisms such as
test_and_set
require
threads
to examine a global shared flag at arbitrary times. This is problematic
and inefficient to implement in modern high-speed processors, where
due to optimization techniques such as pipelining and cached memory,
the contents of memory may not be in a consistent state at every instant.
In
some
multiprocessing systems, therefore, the serializer paradigm
is being supplanted by
other
approaches to concurrency
control.
The problematic aspects of shared state also arise in large, distributed
systems. For instance, imagine a distributed banking system where
individual branch banks maintain local values for bank balances and
periodically compare these with values maintained by other branches. In
such a system the value of the account balance would be
undetermined, except right after synchronization. If Peter deposits money
in an account he holds jointly with Paul, when should we say that the
account balance has changed—when the balance in the local branch
changes, or not until after the synchronization? And if Paul accesses the
account from a different branch, what are the reasonable constraints to
place on the banking system such that the behavior is
correct? The only thing that might matter for correctness
is the behavior observed by Peter and Paul individually and the
state of the account immediately after synchronization.
Questions about the real account balance or the order of
events between synchronizations may be irrelevant or
meaningless.
共有状態の問題は、大規模な分散システムでも顔を出します。たとえば、各支店が口座残高のローカルな値を保持し、定期的にそれを他の支店が保持する値と比較する分散銀行システムを考えてみましょう。このようなシステムでは、口座残高の値は同期した直後を除いて確定しません。Peter が Paul との共同口座にお金を入金したとき、口座残高が変わったと言うべきなのはいつでしょうか。ローカルの支店の残高が変わったときでしょうか、それとも同期が完了するまで待つべきでしょうか。そして Paul が別の支店からその口座にアクセスする場合、システムの振る舞いが正しいと言えるためには、銀行システムにどのような妥当な制約を課すべきでしょうか。正しさにとって意味があるのはおそらく、Peter と Paul がそれぞれ個別に観測する振る舞いと、同期の直後における口座の状態だけです。本当の口座残高や同期と同期のあいだのイベントの順序を問うことは、的外れだったり意味をなさなかったりするかもしれません。
The basic phenomenon here is that synchronizing different
threads,
establishing shared state, or imposing an order on events requires
communication among the
threads.
In essence, any notion of time in concurrency control must be intimately
tied to communication.
It is intriguing that a similar
connection between time and communication also arises in the
Theory of Relativity, where the speed of light (the fastest signal that can
be used to synchronize events) is a fundamental constant relating time and
space. The complexities we encounter in dealing with time and state in our
computational models may in fact mirror a fundamental complexity of
the physical universe.
If the account balances start out as $10,
$20, and $30, then after any number of concurrent exchanges,
the balances should still be $10, $20, and $30 in
some order. Serializing the deposits to individual accounts is not
sufficient to guarantee this. See
exercise 3.43.
The term mutex is an abbreviation for
mutual exclusion. The general problem of arranging a mechanism
that permits concurrent
threads
to safely share resources is called the mutual exclusion problem. Our
mutex is a simple variant of the
semaphore mechanism (see
exercise 3.47), which was introduced in the
THE Multiprogramming System developed at the
Technological University of Eindhoven and named for the university's
initials in Dutch
(Dijkstra 1968a). 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.
mutex という語は
mutual exclusion(相互排他)の略です。並行する
スレッド
がリソースを安全に共有できるようにする機構を構築するという一般的な問題は、相互排他問題と呼ばれます。ここで紹介するミューテックスは
セマフォ機構の単純な一種です(演習 3.47 を参照)。セマフォは
THE
マルチプログラミングシステムの中で導入されました。このシステムは
アイントホーフェン工科大学で開発され、その名前は同大学のオランダ語のイニシャルから取られています
(Dijkstra 1968a)。取得操作と解放操作はもともと
P と V と呼ばれていました。これはオランダ語の passeren(通過する)と
vrijgeven(解放する)から来ており、鉄道で使われていたセマフォ(信号機)にちなんでいます。Dijkstra
の古典的な解説(1968b)は、並行性制御の問題を初めて明確に示したものの一つであり、さまざまな並行性問題をセマフォによって扱う方法を示しました。
In most
time-shared operating systems,
threads
that are
blocked by a mutex do
not waste time
busy-waiting as above. Instead, the system
schedules another
thread
to run while the first is waiting, and the blocked
thread
is awakened when the mutex becomes available.
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
threads
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
thread
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.
このような
命令には、テスト・アンド・セット、テスト・アンド・クリア、スワップ、コンペア・アンド・エクスチェンジ、ロード・リザーブ、ストア・コンディショナルなど、多くの種類があります—その設計はマシンのプロセッサ・メモリインターフェースと注意深く一致させなければなりません。ここで生じる一つの問題は、2つの
スレッド
がそのような命令を使って正確に同時に同じリソースを取得しようとしたら何が起こるかを決めることです。これにはどちらの
スレッド
が制御権を得るかを決定する何らかの機構が必要です。そのような機構は
アービターと呼ばれます。アービターは通常、何らかのハードウェアデバイスに行き着きます。残念ながら、アービターに決定のために任意の長さの時間を与えるのでない限り、100%
の時間で動作する公平なアービターを物理的に構築することはできないことが証明できます。ここでの根本的な現象は、もともと14世紀のフランスの哲学者
Jean Buridan が
アリストテレスの De caelo
に対する注釈の中で観察したものです。Buridan は、完全に合理的な
犬を等しく魅力的な2つの食料源の間に置くと、どちらに先に行くかを決められないために飢え死にすると論じました。
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
threads
back out of the deadlocked state and try again.
Deadlock-recovery mechanisms are widely used in
data-base-management systems, a topic that is treated in detail in
Gray and Reuter 1993.
共有リソースに番号を付けて順番に取得することでデッドロックを回避するという一般的な技法は、Havender (1968) によるものです。デッドロックを回避できない状況ではデッドロックからの回復の方法が必要となり、これは
スレッド
にデッドロック状態から後戻りさせて再試行させるというものです。デッドロックからの回復のメカニズムはデータベース管理システムで広く使われており、これについては Gray and Reuter 1993 で詳しく扱われています。
One such alternative to serialization is called
barrier synchronization. The programmer permits concurrent
threads
to execute as they please, but establishes certain synchronization points
(barriers) through which no
thread
can proceed until all the
threads
have reached the barrier.
Some
processors provide machine instructions
that permit programmers to establish synchronization points at places where
consistency is required. The
PowerPC$^{\textrm{TM}}$, for example, includes
for this purpose two instructions called
SYNC and
EIEIO (Enforced In-order Execution of Input/Output).
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.
For distributed systems, this perspective
was pursued by
Lamport (1978), who showed how to use communication to establish
global clocks that can be used to establish orderings on
events in distributed systems.