CPL 2016, week 2 Inter-thread synchronization: locks and monitors - - PowerPoint PPT Presentation
CPL 2016, week 2 Inter-thread synchronization: locks and monitors - - PowerPoint PPT Presentation
CPL 2016, week 2 Inter-thread synchronization: locks and monitors Oleg Batrashev Institute of Computer Science, Tartu, Estonia February 15, 2016 Agenda Inter-thread synchronization Atomicity and consistency Unconditional locking Locking
Agenda
Inter-thread synchronization Atomicity and consistency Unconditional locking Locking patterns and problems
Simple patterns Deadlocks Refining locks
Conditional locking Cost of synchronization
Atomicity
It is sometimes needed for a block of code
◮ to run operations atomically, i.e. “all at once” ◮ other threads do not interfere:
◮ this thread sees all values as frozen at the start of the block ◮ other threads do not see changes until the whole block finishes
◮ as-if-serial execution
Atomicity for single variable
Increment a variable from several threads
volatile int c = 0 ... c = c+1; // or ++c;
One possible order of actions (the variable is read into register)
// Thread1 r1 = c r = r1 + 1 c = r1 // Thread2 r2 = c r2 = r2 + 1 c = r2
is incorrect, because it results in increment by one: c=c+1. The solution:
◮ prohibit other threads from changing the variable during
read/increment/write
Atomic variables
Easiest way to solve the above problem is to use atomic variables
AtomicInteger c = new AtomicInteger (0); ...
- c. incrementAndGet ()
◮ atomically increments the variable
Other atomic classes and methods:
◮ AtomicBoolean, AtomicLong, AtomicReference ◮ getAndSet(V) – no values are lost in updates, important for
- bject de-initialization
◮ compareAndSet(expect, update) – do not update if
something is different, important if the state has changed
◮ weakCompareAndSet(expect, update) – no ordering
guarantees for other variables!
◮ lazySet() – eventually sets the value
Atomicity for multiple variables
Buffer with internal array and size fields:
void addElement (Element e) { size += 1; array[size -1] = e; }
Someone may try to read the last element which is not yet set:
Element getLastElement () { return array[size -1]; } ◮ reordering the two statements in addElement helps only if:
◮ size is volatile and you update it last in the addElement! ◮ there is only single writer.
Consistency
An object may have only limited set of consistent states:
◮ its fields must correspond to each other ◮ e.g. having matching array and size fields in the buffer
An action could have
◮ precondition: must initially see the object in consistent state ◮ postcondition: must leave the object in consistent state
There are extra conditions may be available as will be shown in Conditional Locks
◮ consistency is the responsibility of the application ◮ concurrency makes it more difficult to provide consistency
Optimistic vs Pessimistic locking
Transition between consistent states may happen with two methods:
◮ pessimistic: make sure nobody interferes and then run the
action
◮ optimistic: run the action locally and publish the results if
nobody has interfered, otherwise restart This corresponds to:
◮ locks (mutexes) – the topic of this section ◮ transactions – the topic to be studied with Software
Transaction Memory in Clojure We continue with pessimistic locking
Locks
Locks are used to manage access to critical sections:
◮ lock is an entity that may be acquired and released ◮ only one thread may acquire the same lock at a time (mutex) ◮ a thread must wait if another thread is holding the lock the
first thread tries to acquire
◮ lock variations:
◮ reentrant – may be acquired multiple times by the same
thread, must be released as many times
◮ read-write – many readers may hold the lock
in Java:
◮ every object has its own lock entity ◮ synchronized block is used to acquire/release object lock synchronized ( objectWithLock ) { // acquire the lock // execute code } // release the lock
Critical sections
Protect code blocks that require atomic execution (Java):
void addElement (Element e) { synchronized (theLock) { // enter critical section size += 1; array[size -1] = e; } // leave critical section: releasing theLock } ◮ change size and array at once, so no other thread interferes
Protect read code too to see a consistent state:
Element getElement () { synchronized (theLock) { return array[size -1]; } } ◮ do not enter if any other thread is already executing this or the
above critical section
◮ forgetting to lock here makes it all meaningless:
◮ general rule: lock at every access!
Java synchronized
Synchronized block is a structural way to locking:
◮ secure – protected against forgetting to unlock and exceptions,
as in
l.acquire (); try { compute (); } finally {l.release (); } ◮ not flexible – may not lock in one method and unlock in
another Java specifics:
◮ Java locks are re-entrant but not interruptible ◮ synchronized void methodA() {} is equivalent to void methodA () { synchronized (this) { } } ◮ synchronized static void methodA() {} is equivalent to static void methodA () { synchronized (ThisC.class) { } }
Java ReentrantReadWriteLock
It is useful:
◮ split readers and writers, so that readers may execute
simultaneously
◮ only single writer is allowed and this implies no readers
There are more to locks, e.g. Java read-write lock:
◮ non-fair or fully fair – acquisition order is not specified or by
arrival
◮ no reader or writer preference is possible
◮ reentrant ◮ lock downgrading ◮ support interruption during lock acquisition
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadW
Fully synchronized objects
◮ every access to an object is takes the object lock – every
method is synchronized
◮ java.util.Collections methods synchronizedMap,
synchronizedList Problems, sometimes:
◮ need lock compound operations, e.g. the following may fail if (list.size () >0) v = list.get (0); ◮ do not need to synchronize every access but prefer to lock for
a set of operations Solution for the first problem:
◮ client may use additional locking, because we know the object
uses the same lock
synchronized (list) { if (list.size () >0) v = list.get (0); }
Client side locking
◮ user of an object is required to lock before using the object ◮ more flexible but also error-prone
◮ may forget to take the lock or use different one
Java synchronized collections (Vector, Collections.synchronized*):
◮ do not provide synchronized looping over the collection ◮ client must synchronize while iterating synchronized (coll) { for (Element e: coll) { // coll.iterator () is used implicitly .. } } ◮ versioned iterators throw
ConcurrentModificationException if the collection is changed during iteration
Copy-on-iteration and Copy-on-write
Copy-on-iteration:
◮ for each iteration copy the collection while holding the lock ◮ release the lock and iterate over the copy ◮ Advantage: lock is held for a short period ◮ Disadvantage: may be out of date and require lots of copying
Copy-on-write:
◮ for each change copy the collection while holding the lock ◮ modify the copy and replace the reference ◮ Advantage: no need to lock or copy for iteration ◮ Disadvantage: may require lots of copying when changed
Copy-on-write is often used for listener/observer lists, because they are seldom modified.
Deadlocks
Pessimistic locking has one serious danger:
◮ thread 1 takes locks A and B in this order ◮ thread 2 takes locks A and B in the opposite order ◮ they may get into deadlock – no thread may proceed, because
waiting for each other resources
code A code B
◮ lines signify executing threads
Ordering locks
One solution to deadlocks:
◮ order locks by any parameter, e.g. A,B,C ◮ never acquire higher priority locks if already holding lower
priority locks
◮ i.e. never try to acquire A if already holding B ◮ may acquire C if already holding B
For example:
synchronized (list) { Element e = list.get (0); synchronized (e) { // change e } } ◮ never try to acquire list lock while holding an element lock!
Reducing and splitting locks
Reducing critical area:
◮ do not hold the lock if not needed: take late, release early ◮ try not to hold the lock for long operations ◮ release and re-acquire if possible
Splitting lock:
◮ use 2 or more locks instead of one if possible:
◮ locks protect independent data/logic
◮ just use Java object for a lock lock1 = new Object() ◮ use as in synchronized(lock1) ...
Collection access example
This example presents reducing, splitting and ordering locks:
◮ maintain lock for list and separate locks for the elements ◮ order locks, so that list lock is always taken first ◮ for insertion/removal of element take the list lock ◮ when changing the element:
- 1. take the list lock and get the element
- 2. take the element lock and release the list lock
- 3. change the element
- 4. release the element lock
list.lock (); elem = list.get(i); elem.lock (); list.unlock ();
- elem. someSeriousModification ()
elem.unlock (); ◮ efficient: each element may be modified in parallel ◮ error-prone: need to ensure correct behavior in case of
exceptions
Polling for condition
When not knowing better solution, polling may be used:
while (true) { synchronized (obj.lock) { if ( conditionSatisfied (objLock) { // do the change break; // exit the loop } } Thread.sleep (10); // wait without holding the lock! for (int i=0; i <1000000; i++); // another method - spinning } ◮ 10ms may be too large and cause unnecessary delays ◮ periodic wake-ups additionally loads the OS and CPU ◮ takes CPU completely while spinning
◮ spin locks are acceptable, even preferred, for some situations
◮ may miss the waiting condition (signal)
Sleep and interrupt
Programmers sometimes may come up with invalid solution:
◮ Thread.sleep() is exited on thread.interrupt(). Why
not to use it?
◮ Almost the same as before: while (true) { synchronized (objLock) { if ( conditionSatisfied (objLock) { // do the change break; // exit the loop } } try { Thread.sleep (1000); // interrupt us from another thread } catch ( InterruptedException e) {} // ignore } ◮ signal may be missed between releasing the lock and
Thread.sleep()!
◮ the two operations must be atomic - see Object.wait()
◮ interrupt() is for thread/executor shutdown and task
cancellation (topic of the next week)
Java monitors
◮ Every Java object has a monitor = lock + condition variable ◮ Object.wait() atomically releases the lock and
goes to sleep
◮ upon wake-up the lock is automatically re-acquired
◮ notify(), notifyAll() wake up one or all waiting threads
Typical usage: Condition waiting thread
synchronized (obj) { while (! obj. isSatisfied ())
- bj.wait ();
- bj.doTheWork ();
}
Notifying thread
synchronized (obj) { doSomething ();
- bj.notifyAll ();
// alternative
- bj.notify ()
} ◮ always re-check the condition upon wake-up ◮ careful with notify() – other threads may also need to wake
up
Monitor example
Classical example for blocking bounded queue:
◮ Reader takes element from the queue if it is not empty synchronized (queue) { while(queue.isEmpty ()) queue.wait (); elem = queue.take (); queue.notifyAll (); } ◮ Writer puts element if it is not full synchronized (queue) { while(queue.isFull ()) queue.wait (); queue.put(elem ); queue.notifyAll (); }
Drawbacks:
◮ all readers and writers wake up each time - could use different
condition variables to separate the two
Condition variables
Allow to have several condition variables for single lock.
◮ Java example for blocking bounded queue from
java.concurrent.locks.Condition documentation
final Lock lock = new ReentrantLock (); final Condition notFull = lock. newCondition (); final Condition notEmpty = lock. newCondition (); ◮ await() instead of wait(), signal() instead of notify()
Additional optimizations:
◮ use notifyAll() only when leaving full or empty states ◮ wake up single thread with notify() if we know only one
thread may successfully proceed after wake-up
◮ e.g. we add/remove only one element at a time
If a thread wants to read simultaneously from two queues:
◮ share a single monitor and check the queues in sequence
Standard synchronizers
Some existing Java classes from java.util.concurrent:
◮ CountDownLatch – wait until certain set of operations is
finished
◮ Semaphore – lock with more than one permit to enter ◮ Exchanger – swap values between threads ◮ BlockingQueue – standard queue with wait/notify ◮ PriorityBlockingQueue – queue with element priorities ◮ DelayQueue – element can be taken only when its delay is
expired
◮ SynchronousQueue – writers and readers wait for each other,
like channels in Go
◮ CopyOnWriteArraySet – copies the set in each modification ◮ AbstractQueuedSynchronizer – base class for user-specific
synchronizers
Lock mechanics and contention
◮ lock acquisition starts with boolean
AtomicInteger.compareAndSet(0,1)
◮ on true return we have taken the lock ◮ on false return somebody else holds it
◮ fairly cheap – same as volatile access, cache line in write mode ◮ if not successful thread needs to be registered to OS mutex
and put to sleep through OS scheduler mechanisms
◮ quite expensive
◮ sometimes it is reasonable to spinlock for a microsecond, in
case the lock will be freed shortly
◮ only then go to sleep through OS