Concurrency 2
Concurrency 2
Construction
Fall 2022
SAFE
CONCURRENCY
Adapted from: 6.031 Fall 2019 at MIT
Rida Assaf
Department of Computer
1
Science
THREAD SAFETY
STRATEGIES
There are basically four ways to make variable access safe in shared-memory
concurrency:
7
1. CONFINEMENT -
EXAMPLE
2- Main creates a second thread using the anonymous Runnable
idiom, and starts that thread.
8
1. CONFINEMENT -
EXAMPLE
3- At this point, we
have two concurrent
threads of execution.
Their interleaving is
unknown! But one
possibility for the next
thing that happens is
that thread 1 enters
computeFact.
9
1. CONFINEMENT -
EXAMPLE
4- Then, the next thing that might
happen is that thread 2 also
enters computeFact. At this point,
we see how confinement helps
with thread safety. Each
execution of computeFact has
its own n, i, and result
variables, confined to that
thread. The data they point to
are also confined, and
immutable. If the BigInteger
objects were not confined – if
they were aliased from multiple 1
1. CONFINEMENT -
EXAMPLE
5- The computeFact
computations proceed
independently, updating
their respective variables.
1
II.
IMMUTABILITY
II. IMMUTABILITY
• Our second way of achieving thread safety is by
using un-reassignable references and immutable data
types. Immutability tackles the shared- mutable-state
cause of a race condition and solves it simply by
making the shared state not mutable.
• A variable declared final is un-reassignable, and
is safe to access from multiple threads. You can
only read the variable, not write it. Be careful, because
this safety applies only to the variable itself, and we
1
III. THREAD-SAFE
DATATYPES
III. THREAD-SAFE
• DATATYPES
Our third major strategy for achieving thread safety is to store shared mutable data
in existing threadsafe data types.
• When a data type in the Java library is threadsafe, its documentation will
explicitly state that fact. For example, here’s what StringBuffer says:
• Fortunately, just like the Collections API provides wrapper methods that make
collections immutable, it provides another set of wrapper methods to make
collections threadsafe, while still mutable.
• These wrappers effectively make each method of the collection atomic with
respect to the other methods. An atomic action effectively happens all at
once – it doesn’t interleave its internal operations with those of other
actions, and none of the effects of 1 the action are visible to other threads
SUMMARY SO FAR
2
SYNCHRONIZATION
DEFINITION
2
LOCKS CONT’D
Locks have two operations:
And then think about what happens So A is holding Harry and waiting
for Snape, and B is holding Snape
when two independent threads are and waiting for Harry. Both
repeatedly running: threads are stuck in friend(), so
neither one will ever manage to
exit the synchronized region and
release the lock to the other.This
3
is a classic deadly embrace.The
WIZARD EXAMPLE
• We will deadlock ver y rapidly. Here’s why. abo to
Suppose
harr thread Aand is
y.friend(snape), ut
thread B is about to execute snape.friend(harrexecute
y).
• Thread A acquires the lock on harry (because the friend method is synchronized).
• Then thread B acquires the lock on snape (for the same reason).
• They both update their individual reps independently, and then try to call
friend() on the other object
— which requires them to acquire the lock on the other object.
• So A is holding Harry and waiting for Snape, and B is holding Snape and waiting
for Harry. Both threads are stuck in friend(), so neither one will ever manage to
exit the synchronized region and release the lock to the other.This is a classic
deadly embrace.The program simply stops.
• The essence of the problem is acquiring
3 multiple locks, and holding some of the
DEADLOCK
SOLUTION
#1LOCK ORDERING
• One way to prevent deadlock is to put an ordering on the locks
that need to be acquired simultaneously, and ensuring that all
code acquires the locks in that order.
• In our social network example, we might always acquire the
locks on the Wizard objects in alphabetical order by the wizard’s
name. Since thread A and thread B are both going to need the
locks for Harry and Snape, they would both acquire them in that
order: Harry’s lock first, then Snape’s. If thread A gets Harry’s lock
before B does, it will also get Snape’s lock before B does, because
B can’t proceed until A releases 3Harry’s lock again. The ordering
DEADLOCK
SOLUTION
#1 LOCK ORDERING
3
DEADLOCK
SOLUTION
#1 isDRAWBACKS
Although lock ordering useful (particularly in code like operating
system kernels), it has a number of drawbacks in practice:
• First, it’s not modular — the code has to know about all the locks in
the system, or at least in its subsystem.
• Second, it may be difficult or impossible for the code to know
exactly which of those locks it will need before it even acquires
the first one. It may need to do some computation to figure it
out. Think about doing a depth-first search on the social
network graph, for example — how would you know which nodes
need to be locked, before you’ve3 even started looking for them?
DEADLOCK SOLUTION
#2 COARSE-GRAINED LOCKING
A more common approach than lock ordering, particularly
for application programming (as opposed to operating
system or device driver programming), is to use coarser
locking — use a single lock to guard many object instances,
or even a whole subsystem of a program.
4
DEADLOCK
EXERCISE
• Scenario C:
•Thread 1 running synchronized (beta)
• Thread 2 blocked on synchronized
(gamma)
• Thread 3 blocked on 1st synchronized
(gamma)
4
DEADLOCK
EXERCISE
• Scenario D:
• Thread 1 blocked on synchronized
(beta)
• Thread 2 finished
• Thread 3 blocked on 2nd synchronized
(gamma)
4
DEADLOCK EXERCISE
• In the previous problem,
we saw deadlocks involving
beta and gamma. What about
alpha? Which of the below is
correct :
a)There is a possible
deadlock where thread 1 owns
the lock on alpha.
b)There is a possible
deadlock where thread 2 owns
the lock on alpha.
c)There is a possible
deadlock where thread 3 owns
the lock on alpha. 4
DEADLOCK EXERCISE
• There are no deadlocks involving
alpha.
• We can reason about it this way:
in order to encounter deadlock,
threads must try to acquire locks
in different orders, creating a cycle in
the graph of who-is-waiting-for-who.
• So we look at alpha vs. beta: are
there two threads that try to
acquire these locks in the opposite
order? No. Only thread 2 acquires
them both at the same time.
• Next we look at alpha vs. gamma:
are there two threads that try to
acquire these locks in the opposite
4
order? No. Both thread 2 and
QUEUES AND
MESSAGE
PASSING
SHARED MEMORY VS MESSAGE
PASSING
• In the shared memory model, concurrent modules interact
by reading and writing shared mutable objects in memory.
Creating multiple threads inside a single Java process
is our primary example of shared-memory
concurrency.
• In the message passing model, concurrent modules
interact by sending immutable messages to one another
over a communication channel. That communication
channel might connect different computers over a network,
4
SHARED MEMORY VS MESSAGE
PASSING
• Rather than synchronize with locks, message
passing systems synchronize on a shared
communication channel, e.g. a stream or a queue.
• Threads communicating with blocking queues is a
useful pattern for message passing within a single
process.
4
ADVANTAGES OF MESSAGE
PASSING
• The message passing model has several advantages
over the shared memory model, which boil down to
greater safety from bugs. In message- passing, concurrent
modules interact explicitly, by passing messages through
the communication channel, rather than implicitly
through mutation of shared data.
• Message passing also immuta objects (the
shares only ble messages) requires
objects, which
between we have
modules, already memor
whereas seen can be a
sharing mutable
source of
shared bugs. y
5
MESSAGE PASSING USING
QUEUES
We can use a queue with blocking operations for message passing between threads.
• In an ordinary Queue:
• add(e) adds element e to the end of the queue.
• remove() removes and returns the element at the head of the queue, or throws
an exception if the queue is empty.