In this exercise, we are given three events acting on an initial balance
of 100:-
(set! balance (+ balance 10))
(set! balance (- balance 20))
(set! balance (- balance (/ balance 2)))
When these events might be run sequntally in an arbitrary order, the following values of balance
might be produced:-
If the individual processes can be interleaved, many different values can be produced.
The above ordering can produce a final value of 80.
The above ordering can produce a final value of 110. These examples show us the importance of concurrency management.
In this exercise, we are given the following serialized code:-
We are asked what the possible values of x
would be once the execution is complete. We can see there are three operations:-
((s (lambda () (* x x))))
(set! x ...)
(s (lambda () (set! x (+ x 1))))
Since step 2 is dependant of 1 being complete, and 2 can interleave with 3, we get the following permutations and results:-
In this exercise, we are asked for possible values when the following code is evaluated:-
However, if we uses serialized procedures as follows:-
We can have and getting run atomically. Since the operation is commutative, we get 1000000 in both cases.
In this exercise, Ben Bitdiddle replaces access to balance from balance
to (protected (lambda () balance))
. I think that this replacement is not required. This is mainly because accessing balance
is a single operation. Thus we will not get any other operation interleaving with this access. Furthermore, mutations to balance
is done using serialized methods and that ensures safe and consistent operations. Only place where interference might occur is when one tries to access balance
in the middle of withdraw
and deposit
operations. This technically is not a faulty operation since we only consider the data to be mutated once these operations are over.
In this exercise, we have another change by Ben Bitdiddle, he changes make-account
so that dispatch
returns pre-protected functions instead of protecting function each time its accessed. This is a valid change since protection only comes into play when executing. When the protection is applied is immaterial.
In this exercise, we talk about exchanging balances between three accounts. Let us use the following code as an example:-
If we use the given code as follows and replace exchange
above with serialized-exchange
below:-
we preserve the right balances because all involved accounts are serialized for the duration of exchange. No other process involving either account can be run while serialized-exchange
being run. However, if we use the original exchange
function, we obviously will get errors because execution can be interleaved between when a balance is computed and when balance is swapped. Let us demonstrate this by using the following diagram:-
Though, we should end with 20/30/10, we end up with 20/20/20. This is because by the time actual swap is conducted, the difference
amount to swap would not be valid anymore. However, since the amount withdrawn and deposited in any step is equal and each sub-operation is serialized, the total amount of balance would still be maintained.
In this exercise, we are given the following code to transfer amounts between accounts:-
Ben Bitdiddle calins that the procedure works when many people transfer money concurrently and Louis Reasoner claims otherwise. I would agree with Ben. Since withdraw
and deposit
are serialized, there should not be any inconsistencies. Furthermore, the actual amount
to transfer is held the same across withdrawal and deposit. This means that the intended effect is obtained.
A key difference between this and the previous exercise is that the value of difference
migh be invalid before the transfer ever takes place. In this case, we are explicitly given the transfer amount. Thus no concurrency issues would arise.
In this exercise, we are given a new version of make-account-and-serializer
from Louis that serializes withdraw
and deposit
internally as well as exposes a serializer for use in transactions involving multiple accounts.
The issue with this code is that when a combined transaction needs to perform deposit
or withdraw
, it would be locked out since the overall transaction would already have acquired the locks. For example, let us look at the serialized-exhange
code again:-
In this case, since the overall call to exchange
is serialized with serializer1
and serializer2
, the internal calls would not be able to call withdraw
and deposit
since they require the serializers again. Thus, the call would have a deadlock.
In this exercise, we are asked what happens if the given test-and-set!
process is not run atomically.
The main risk in the above code is when two processes concurrently try to obtain a lock and there is interleaving between test and set. Let us see how this happens. Assume processes and try to obtain a lock at the same time.
(car cell)
and recieves #f
(car cell)
and recieves #f
cell
to #t
and returns #f
cell
to #t
and returns #f
Since there is a break between when the cell is tested and when a lock is actually set, both processes acquire a lock. Thus, it is esssential that test-and-set!
be atomic.
In this exercise, we are tasked with implementing semaphores of size n.
To implement semaphores in terms of mutexes, we simply create a structure that maintains the number of processes that can still acquire the semaphore currently. Then, we can use a built-in mutex
to test and set the count accordingly. If the count is 0, then the semaphore needs to re-acquire just like how we retry for mutex. The following code demonstrates this.
test-and-set!
opeerationsTo implement semaphores in terms of test-and-set!
operations, we can simply replace mutex
above with test-and-set!
. In this case, the retry part of mutex would also need to be implemented. The following code is the implementation.
In this exercise, we are presented with a method for deadlock avoidance. The accounts are numbered and the locks are acquired in order of the account number. This avoids deadlock because all processes acquire locks in the same order. This prevents the case where two concurrent processes are waiting for the other process to release a lock to proceed. The process to obtain the first lock is able to acquire all other locks. The serialized-exchange
incorporating this idea is as follows:-
This problem is famously called the Dining Philosophers problem and this solution was proposed by Edgar Djikstra.
One place where the solution presented would fail is when the resources required are not known in advance. An example would be in case of database mutations where we can have a complex query which needs to find out the row to mutate by reading another row first. In this case, the database cannot know in advance which locks to obtain. Thus, there might be a chance for deadlock to occur.