synchronization primitives

14
Synchronization Primitives Several synchronization primitives have been introduced to aid in multithreading the kernel. These primitives are implemented by atomic operations and use appropriate memory barriers so that users of these primitives do not have to worry about doing it themselves. The primitives are very similar to those used in other operating systems including mutexes, condition variables, shared/exclusive locks, and semaphores. Mutexes The mutex primitive provides mutual exclusion for one or more data objects. Two versions of the mutex primitive are provided: spin mutexes and sleep mutexes. Spin mutexes are a simple spin lock. If the lock is held by another thread when a thread tries to acquire it, the second thread will spin waiting for the lock to be released. Due to this spinning nature, a context switch cannot be performed while holding a spin mutex to avoid deadlocking in the case of a thread owning a spin lock not being executed on a CPU and all other CPUs spinning on that lock. An exception to this is the scheduler lock, which must be held during a context switch. As a special case, the ownership of the scheduler lock is passed from the thread being switched out to the thread being switched in to satisfy this requirement while still protecting the scheduler data structures. Since the bottom half code that schedules threaded interrupts and runs non-threaded interrupt handlers also uses spin mutexes, spin mutexes must disable interrupts while they are held to prevent bottom half code from deadlocking against the top half code it is interrupting on the current CPU. Disabling interrupts while holding a spin lock has the unfortunate side effect of increasing interrupt latency. To work around this, a second mutex primitive is provided that performs a context switch when a thread blocks on a mutex. This second type of mutex is dubbed a sleep mutex. Since a thread that contests on a sleep mutex blocks instead of spinning, it is not susceptible to the first type of deadlock with spin locks. Sleep mutexes cannot be used in bottom half code, so they do not need to disable interrupts while they are held to avoid the second type of deadlock with spin locks.

Upload: haani104

Post on 18-Nov-2014

1.010 views

Category:

Documents


2 download

DESCRIPTION

Synchronization PrimitivesAtomic LocksTicket LocksArray Based LocksBarrier_Central and Tree

TRANSCRIPT

Page 1: Synchronization Primitives

Synchronization Primitives

Several synchronization primitives have been introduced to aid in

multithreading the kernel. These primitives are implemented by atomic

operations and use appropriate memory barriers so that users of these

primitives do not have to worry about doing it themselves. The primitives are

very similar to those used in other operating systems including mutexes,

condition variables, shared/exclusive locks, and semaphores.

Mutexes

The mutex primitive provides mutual exclusion for one or more data objects.

Two versions of the mutex primitive are provided: spin mutexes and sleep

mutexes.

Spin mutexes are a simple spin lock. If the lock is held by another thread when

a thread tries to acquire it, the second thread will spin waiting for the lock to

be released. Due to this spinning nature, a context switch cannot be performed

while holding a spin mutex to avoid deadlocking in the case of a thread owning

a spin lock not being executed on a CPU and all other CPUs spinning on that

lock. An exception to this is the scheduler lock, which must be held during a

context switch. As a special case, the ownership of the scheduler lock is passed

from the thread being switched out to the thread being switched in to satisfy

this requirement while still protecting the scheduler data structures. Since the

bottom half code that schedules threaded interrupts and runs non-threaded

interrupt handlers also uses spin mutexes, spin mutexes must disable

interrupts while they are held to prevent bottom half code from deadlocking

against the top half code it is interrupting on the current CPU. Disabling

interrupts while holding a spin lock has the unfortunate side effect of

increasing interrupt latency.

To work around this, a second mutex primitive is provided that performs a

context switch when a thread blocks on a mutex. This second type of mutex is

dubbed a sleep mutex. Since a thread that contests on a sleep mutex blocks

instead of spinning, it is not susceptible to the first type of deadlock with spin

locks. Sleep mutexes cannot be used in bottom half code, so they do not need

to disable interrupts while they are held to avoid the second type of deadlock

with spin locks.

Page 2: Synchronization Primitives

As with Solaris, when a thread blocks on a sleep mutex, it propagates its

priority to the lock owner. Therefore, if a thread blocks on a sleep mutex and

its priority is higher than the thread that currently owns the sleep mutex, the

current owner will inherit the priority of the first thread. If the owner of the

sleep mutex is blocked on another mutex, then the entire chain of threads will

be traversed bumping the priority of any threads if needed until a runnable

thread is found. This is to deal with the problem of priority inversion where a

lower priority thread blocks a higher priority thread. By bumping the priority of

the lower priority thread until it releases the lock the higher priority thread is

blocked on, the kernel guarantees that the higher priority thread will get to run

as soon as its priority allows.

These two types of mutexes are similar to the Solaris spin and adaptive

mutexes. One difference from the Solaris API is that acquiring and releasing a

spin mutex uses different functions than acquiring and releasing a sleep mutex.

A difference with the Solaris implementation is that sleep mutexes are not

adaptive. Details of the Solaris mutex API and implementation can be found in

section 3.5 of [Mauro01].

Condition Variables

Condition variables provide a logical abstraction for blocking a thread while

waiting for a condition. Condition variables do not contain the actual condition

to test, instead, one locks the appropriate mutex, tests the condition, and then

blocks on the condition variable if the condition is not true. To prevent lost

wakeups, the mutex is passed in as an interlock when waiting on a condition.

FreeBSD's condition variables use an API quite similar to those provided in

Solaris. The only differences being the lack of a cv_wait_sig_swap and the

addition of cv_init and cv_destroy constructors and destructors. The

implementation also differs from Solaris in that the sleep queue is embedded

in the condition variable itself instead of coming from the hashed pool of sleep

queue's used by sleep andwakeup.

Shared/Exclusive Locks

Shared/Exclusive locks, also known as sx locks, provide simple reader/writer

locks. As the name suggests, multiple threads may hold a shared lock

simultaneously, but only one thread may hold an exclusive lock. Also, if one

thread holds an exclusive lock, no threads may hold a shared lock.

Page 3: Synchronization Primitives

FreeBSD's sx locks have some limitations not present in other reader/writer

lock implementations. First, a thread may not recursively acquire an exclusive

lock. Secondly, sx locks do not implement any sort of priority propagation.

Finally, although upgrades and downgrades of locks are implemented, they

may not block. Instead, if an upgrade cannot succeed, it returns failure, and

the programmer is required to explicitly drop its shared lock and acquire an

exclusive lock. This design was intentional to prevent programmers from

making false assumptions about a blocking upgrade function. Specifically, a

blocking upgrade must potentially release its shared lock. Also, another thread

may obtain an exclusive lock before a thread trying to perform an upgrade. For

example, if two threads are performing an upgrade on a lock at the same time.

Semaphores

Semaphore is used as a synchronization tool. A semaphore S is an integer

variable that, apart from initialization, is accessed only through two standard

atomic operations: wait and signal. P(for Wait) and V(for Signal).

The classical definition of Wait in pseudocode is

Wait(S) {

While(S<=0)

;//no-op

S--;

}

For Signal

Signal(S){

S++;

} When one process modifies the semaphore value, no other process can

simultaneously modify that same semaphore value.

Page 4: Synchronization Primitives

Atomic Primitives

Atomic primitives are arguably the most important tool in programming that

requires coordination between multiple threads and/or processors/cores.

There are four basic types of atomic primitives: swap, fetch and phi, compare

and swap, and load linked/store conditional. Usually these operations are

performed on values the same size as or smaller than a word (the size of the

processor's registers), though sometimes consecutive (single address)

multiword versions are provided.

The most primitive (pun not intended) is the swap operation. This operation

exchanges a value in memory with a value in a register, atomically setting the

value in memory and returning the original value in memory. This is not

actually very useful, in terms of multiprogramming, with only a single practical

use: the construction of test and set (TAS; or test and test and set - TATAS)

spinlocks. In these spinlocks, each thread attempting to enter the spinlock

spins, exchanging 1 into the spinlock value in each iteration. If 0 is returned,

the spinlock was free, and the thread now owns the spinlock; if 1 is returned,

the spinlock was already owned by another thread, and the thread attempting

to enter the spinlock must keep spinning. Ultimately, the owning thread leaves

the spinlock by setting the spinlock value to 0.

C/C++ - Code

1. ATOMIC word swap(volatile word &value, word newValue)

2. {

3. word oldValue = value;

4. value = newValue;

5.

6. return oldValue;

7. }

8.

Page 5: Synchronization Primitives

9. void EnterSpinlock(volatile word &value)

10. { 11. // Test and test and set spinlock

12. while (value == 1 || swap(value, 1) == 1) {}

13. } 14. 15. void LeaveSpinlock(volatile word &value)

16. { 17. value = 0;

18. }

This makes for an extremely crude spinlock. The fact that there is only a single

value all threads share means that spinning can create a lot of cache coherency

traffic, as all spinning processors will be writing to the same address,

continually invalidating each other's caches. Furthermore, this mechanism

precludes any kind of order preservation, as it's impossible to distinguish when

a given thread began waiting; threads may enter the spinlock in any order,

regardless of how long any thread has been waiting to enter the spinlock.

Next up the power scale is the fetch and phi family. All members of this family

follow the same basic process: a value is atomically read from memory,

modified by the processor, and written back, with the original value returned

to the program. The modification performed can be almost anything; one of

the most useful modifications is the add operation (in this case it's called fetch

and add). The fetch and add operation is notably more useful than the swap

operation, but is still less than ideal; in addition to test and set spinlocks, fetch

and add can be used to create thread-safe counters, and spinlocks that both

preserve order and (potentially) greatly reduce cache coherency traffic.

1. ATOMIC word fetch_and_add(volatile word &value, word addend)

2. {

3. word oldValue = value;

4. value += addend;

5.

6. return oldValue;

7. }

8.

Page 6: Synchronization Primitives

9. void EnterSpinlock (volatile word &seq, volatile word &cur)

10. { 11. word mySeq = fetch_and_add(seq, 1);

12. while (cur != mySeq) {}

13. } 14. 15. void LeaveSpinlock(volatile word &cur)

16. { 17. cur++;

18. }

Next is the almost ubiquitous compare and swap (CAS) operation. In this

operation, a value is read from memory, and if it matches a comparison value

in the processor, a third value in the processor is atomically written in the

memory location, and ultimately the original value in memory is returned to

the program. This operation is very popular because it allows you to perform

almost any operation on a single memory location atomically, by reading the

value, modifying it, and then compare-and-swapping it back to memory. For

this reason, it is considered to be a universal atomic primitive.

1. ATOMIC word compare_and_swap(volatile word &value, word

newValue, word comparand)

2. {

3. word oldValue = value;

4. if (oldValue == comparand)

5. value = newValue;

6.

7. return oldValue;

8. }

Some architectures (such as the x86) support double-width compare and swap

(atomic compare and swap of two consecutive words). While this is

convenient, it should not be relied upon in portable software, as many

architectures do not support it. Note that double-width compare and swap is

NOT the same as double compare and swap (which we'll look at soon).

Another universal atomic primitive is the load linked/store conditional (LL/SC)

pair of instructions. In this model, when a value is loaded linked into a register,

Page 7: Synchronization Primitives

a reservation is placed on that memory address. If that reservation still exists

when the store conditional operation is performed, the store will be

performed. If another thread writes to the memory location between a load

linked and store conditional, the reservation is cleared, and any other threads

will fail their store conditional (resulting in skipping the store altogether). If the

store conditional fails, the program must loop back and try the operation

again, beginning with the load linked.

In theory, the load linked/store conditional primitive is superior to the

compare and swap operation. As any access to the memory address will clear

the reservations of other processors, the total number of reads is 1, in contrast

to the 2 reads with compare and swap (1 read to get the value initially, and a

second read to verify the value is unchanged). Furthermore, the LL/SC

operation may distinguish whether a write has occurred, regardless of whether

the value was changed by the write (we'll come back to why this is a good

thing when we discuss hazard pointers). Unfortunately, my research indicates

that most implementations of LL/SC do not guarantee that a write will be

distinguished if the written value is the same as the value at the time of the

reserve.

Finally, the wild card: the double compare and swap (DCAS). In a double

compare and swap, the compare and swap operation is performed

simultaneously on the values in two memory addresses. Obviously this

provides dramatically more power than any previous operation, which only

operate on single addresses. Unfortunately, support for this primitive is

extremely rare in real-world processors, and it is typically only used by lock-

free algorithm designers that are unable to reduce their algorithm to single-

address compare and swap operations.

1. ATOMIC void double_compare_and_swap(volatile word &value1,

word newValue1, word comparand1, word &oldValue1, volatile

word &value2, word newValue2, word comparand2, word

&oldValue2)

2. {

3. oldValue1 = value1;

4. oldValue2 = value2;

5.

6. if (oldValue1 == comparand1 && oldValue2 == comparand2)

Page 8: Synchronization Primitives

7. {

8. value1 = newValue1;

9. value2 = newValue2;

10. }

11. }

Lastly, note that I'm using rather loose classification of these primitives.

Technically, you could place the swap (fetch and store) operations in the fetch

and phi family, as well, but it seemed more intuitive to me to separate them.

Ticket Lock

A ticket lock is a form of lockless inter-thread synchronization.

Overview

Conventionally, inter-thread synchronization is achieved by using

synchronization entities provided by the operating system, such as events,

semaphores and mutexes.For example, a thread will create a mutex and in the

act of creation, "claim" the mutex. A mutex can have only one owner at any

time. Other threads, when they come to the point where their behaviour must

be synchronized, will attempt to "claim" the mutex; if they cannot, because

another thread already owns the mutex, they automatically sleep until the

thread which currently owns the mutex gives it up. Then one of the currently

sleeping threads will automatically be awoken and given ownership of the

mutex.

A ticket lock works as follows; there are two integer values which begin at 0.

The first value is the queue ticket, the second is the dequeue ticket.

When a thread arrives, it atomically obtains and then increments the queue

ticket. It then atomically compares its ticket with the dequeue ticket. If they

are the same, the thread is permitted to enter the serialised code. If they are

not the same, then another thread must already be in the serialised code and

this thread must busy-wait or yield. When a thread has comes to leave the

serialised code, it atomically increments the dequeue ticket, thus permitting

the next waiting thread to enter the serialised code.

This approach is heavyweight (high-overhead, code intensive), in that such

entities have a significant impact upon the performance of the operating

Page 9: Synchronization Primitives

system, since the operating system has to switch into a special mode when

dealing with operations upon synchronization entities to ensure they provide

synchronized behaviour.

A further drawback is that if the thread which owns the mutex fails, the entire

application halts. (This type of problem applies to all synchronization entities).

Lockless locking

Main article: Lock-free and wait-free algorithms

Lockless locking achieves inter-thread synchronization without the use of

operating system provided sychronization entities.Generally, two techniques

are involved.

The first technique is the use of a special set of instructions which are

guaranteed atomic by the CPU. This generally centers on an instrument known

as Compare-and-swap. This instruction compares two variables and if they are

the same, replaces one of the variable's values with a third value.

For example;compare_and_swap ( destination, exchange, comparand );

Here the destination and comparand are compared. If they are identical, the

value in exchange is placed in destination.

This first technique provides the vital inter-thread atomicity of operation. It

would otherwise be impossible to perform any lockless locking, since on

systems with multiple processors, even operations such as a simple assignment

(let a = b) could be half-way through occurring while other operations occur on

other processors; the software trying to sychronize across processors could

never be sure that any operation had reached a sane state permitting it to be

used in some way.

The second technique is to busy-wait or yield when it is clear that the current

thread cannot be permitted to continue processing. This provides the vital

ability to defer the processing done by a thread when such processing would

violate the operations which are being serialised between threads.

Page 10: Synchronization Primitives

Test & Test & Set Lock

In computer science, the test-and-set CPU instruction is used to

implement mutual exclusion in multiprocessor environments. Although a

correct lock can be implemented with test-and-set, it can lead to memory

contention in busy lock (caused by bus locking and cache invalidation when

test-and-set operation needs to access memory atomically).

To lower the overhead a more elaborate locking protocol test and test-and-

set is used. The main idea is not to spin in test-and-set but increase the

likelihood of successful test-and-set by using the following entry protocol to

the lock:

boolean locked := false // shared lock variable procedure EnterCritical()

{ do { while (locked == true) skip // spin until

lock seems free } while TestAndSet(locked) // actual atomic locking }

Exit protocol is:

procedure ExitCritical() { locked := false }

The entry protocol uses normal memory reads to spin, waiting for the lock to

become free. Test-and-set is only used to try to get the lock when normal

memory read says it's free. Thus the expensive atomic memory operations

happens less often than in simple spin around test-and-set.

If the programming language used supports short-circuit evaluation, the entry

protocol could be implemented as:

procedure EnterCritical() { while ( locked == true or TestAndSet(locked) == true

) skip // spin until locked }

Page 11: Synchronization Primitives

Array Based Locks

In array-based locks each lock is implemented as an array of size p where p is

the total number of processors. The lock acquire involves atomically copying

and incrementing a shared index variable and spinning on the array location

indexed by the copied value. Releasing a lock involves resetting the

corresponding array location and setting the next array location if a processor

is waiting. Each array location should be allocated on a different cache line to

avoid invalidations due to false-sharing. Assume a large enough cache line size

(say, 256 bytes) and put that amount of padding between consecutive lock

locations. Use LL/SC for implementing any atomic code section that may be a

part of lock acquire or release. The algorithm should be written in C with calls

to atomic primitives implemented with LL/SC in MIPS assembly.

It Solves the O (P2) traffic problem.

Barriers

Race conditions can cause a program to execute in a non-deterministic fashion,

producing inconsistent results. Synchronization routines are used to remove

race conditions from a code. In certain cases all threads must have executed a

portion of code before continuing. Barrier synchronization is a technique to

achieve this. A thread executing an episode of a barrier waits for all other

threads before proceeding to the next. Therefore, when a barrier is reached,

all threads are forced to wait for the last thread to arrive. Use of barriers is

common in shared memory parallel programming.

Some terminologies in the context of this report:

1. Threads – Units of execution initiated by the program using openMp which

might run on different processors but on the same parallel machine in which

they were initiated.

2. Parallel Machine – A collection of processors which are tightly coupled and

use shared memory. Jedi1 is considered a parallel machine. It has 4 processors.

3. Processor – A subunit of a parallel machine.

Two types of Barrier are

1.Centralized Barrier

2.Tree Barrier

Page 12: Synchronization Primitives

Centralized Barrier

Each of the N threads entering the barrier atomically decrements a shared

integer, the initial value of which is N. If the value of the integer after

decrement is 0 then the thread resets the counter and changes a shared

central flag. Otherwise, the thread waits for notification. Arriving processor

decrements “count” and then wait until “sense” has a different value than it

had in the previous barrier. The last arriving processor resets “count” and

reverses “sense”. The algorithm relies on every thread reading and writing to a

single memory location: the counter. This memory location is known as a hot-

spot.

Tree Barrier

Does not need a lock, only uses flags

-Arrange the processors logically in a binary tree (higher degree also possible)

-Two siblings tell each other of arrival via simple flags (i.e. one waits on a flag

while the other sets it on arrival)

-One of them moves up the tree to participate in the next level of the barrier

- Introduces concurrency in the barrier algorithm since independent subtrees

can proceed in parallel

- Takes log(P) steps to complete the acquire

- A fixed processor starts a downward pass of release waking up other

processors that in turn set other flags

Page 13: Synchronization Primitives

- Shows much better scalability compared to centralized barriers in DSM

multiprocessors; the advantage in small bus-based systems is not much, since

all transactions are any way serialized on the bus; in fact the additional log (P)

delay may hurt performance in bus-based SMPs

Binary Barrier Tree Signal

Binary barrier tree wakeup

Each processor will need at most log (P) + 1 flags

Avoid false sharing: allocate each processor’s flags on a separate chunk of

cache lines

With some memory wastage (possibly worth it) allocate each processor’s flags

on a separate page and map that page locally in that processor’s physical

memory.

Avoid remote misses in DSM multiprocessor

Does not matter in bus-based SMPs

Page 14: Synchronization Primitives

What is Sense Reversal?

• Problem: reinitialization

—each time a barrier is used, it must be reset

• Difficulty: odd and even barriers can overlap in time

—some processes may still be exiting the kth barrier

—other processes may be entering the k+1st barrier

—how can one reinitialize?

• Solution: sense reversal

—terminal state of one phase is initial state of next phase

—e.g.

– odd barriers wait for flag to transition from true to false

– even barriers wait for flag to transition from false to true

• Benefits

—reduce number of references to shared variables

—eliminate one of the spinning episodes