CPL 2016, week 2 Inter-thread synchronization: locks and monitors - - PowerPoint PPT Presentation

cpl 2016 week 2
SMART_READER_LITE
LIVE PREVIEW

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


slide-1
SLIDE 1

CPL 2016, week 2

Inter-thread synchronization: locks and monitors Oleg Batrashev

Institute of Computer Science, Tartu, Estonia

February 15, 2016

slide-2
SLIDE 2

Agenda

Inter-thread synchronization Atomicity and consistency Unconditional locking Locking patterns and problems

Simple patterns Deadlocks Refining locks

Conditional locking Cost of synchronization

slide-3
SLIDE 3

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

slide-4
SLIDE 4

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

slide-5
SLIDE 5

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

slide-6
SLIDE 6

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.

slide-7
SLIDE 7

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

slide-8
SLIDE 8

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

slide-9
SLIDE 9

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

slide-10
SLIDE 10

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!

slide-11
SLIDE 11

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) { } }

slide-12
SLIDE 12

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

slide-13
SLIDE 13

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); }

slide-14
SLIDE 14

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

slide-15
SLIDE 15

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.

slide-16
SLIDE 16

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

slide-17
SLIDE 17

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!

slide-18
SLIDE 18

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) ...

slide-19
SLIDE 19

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

slide-20
SLIDE 20

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)

slide-21
SLIDE 21

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)

slide-22
SLIDE 22

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

slide-23
SLIDE 23

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

slide-24
SLIDE 24

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

slide-25
SLIDE 25

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

slide-26
SLIDE 26

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

Is locking cheap?

◮ relatively yes, unless many threads contend for the lock and

therefore put to sleep often!

slide-27
SLIDE 27

Conclusions

◮ many actions in the program need to be atomic ◮ concurrent programs need synchronization to preserve

consistent states

◮ pessimistic scheme works by taking locks ◮ synchronized block is a structured way to locking ◮ reducing locking is good for performance ◮ ordering locks is necessary for deadlock-free code ◮ monitors allow to wait for a condition, accessed through the

lock

◮ Java5 and newer provide state-of-the-art synchronization

classes