Locks : The Basic Idea
- Ensure that any critical section executes as if it were a single atomic instruction.
Add some code around the critical section
lock_t mutex; // some globally-allocated lock 'mutex'
...
lock(&mutex);
balance=balance+1;
unlock(&mutex);
- Lock variable holds the state of the lock
- available (or unlocked or free)
- No therad holds the lock
- acquired (or locked or held)
- Exactly one thread holds the lock presumably is in a critical section.
- available (or unlocked or free)
lock은 기본적으로 임계 지점에서 thread간의 충돌을 막기 위해 사용된다.
lock의 상태에는 available( unlocked, free) 그리고 acquired(locked or held)로 나뉜다.
The semantics of the lock()
lock()
- Try to acquire the lock
- If no other thread holds the lock, the thread will acquire the lock.
- Enter the critical section
- This thread is said to be the owner of the lock.
- Other threads are prevented from entering the critical section while the first thread that holds the lock is in there.
lock()이 실행되었을 때 다른 thread가 lock을 hold하지 않는다면, lock을 acquire하고 critical sectioin으로 들어가겠다는 의미이다.
Pthread Locks - mutex
- The name that the POSIX library uses for a lock
- Used to provide mutual exclusion between threads.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
Pthread_mutex_lock(&lock);
balance = balance + 1;
Pthread_mutex_unlock(&lock);
- We may be using different locks to protect different variables -> Increase concurrency (a more fine-grained approach)
Building A Lock
- Efficient locks provided mutual exclusion at low cost.
- Building a lock need some help from the hardware and the OS.
Evaluating locks - Basic criteria
- Mutual exclusion
- Does the lock work, preventing multiple threads from entering a critical section?
- Fairness
- Does each thread contending for the lock get a fair shot at acquiring it once it is free?(Starvation)
- Performance
- The time overheads added by using the lock
Lock을 평가할 때는 3가지 기준이 있다.
Fairness가 의미하는 것은 특정 thread만이 lock이 되어 starvation 이 오지 않게 구현하라는 것을 의미한다.
Controlling Interrupts
- Disable Interrupts for critical sections
- One of the earliest solutions used to provide mutual exclusion
- Invented for single-processor systems
void lock(){ DisableInterrupts(); } void unlock(){ EnableInterrupts(); }
- Problem
- Require too much trust in applications
- Greedy( or malicious ) program could monopolize the processor.
- Do not work on multiprocessor.
- Code that masks or unmasks interrupts be executed slowly by modern CPUs.
- Require too much trust in applications
위 처럼 코드를 짜게 되면 문제점이 많다.
일단 코드를 전적으로 믿어야 한다. 만약 lock을 걸어 놓고 해당 thread를 block한다고 가정하면 interrupt를 사용할 수 없는 상태에서 thread_block()이 일어나므로 해당 프로그램은 영원히 sleep될 것이다.
Why hardware support needed?
- First attempt : Using a flag denoting whether the lock is held or not.
-
typedef struct __lock_t {int flag; } lock_t; void init(lock_t *mutex){ // 0 -> lock is available, 1->held mutex->flag = 0; } void lock(lock_t *mutex){ while(mutex->flag==1) // TEST the flag ; //spin-wait (do nothing) mutex->flag = 1; // now SET it ! } void unlock(lock_t *mutex){ mutex_flag = 0; }
위는 flag를 이용하여 만약 flag==1이어서 접근 못하는 경우 while 문을 돌면서 기다렸다가 while이 flag!=1이면 자신이 flag를 1로 만들고 들어가는 식으로 구현하였는데 몇가지 문제가 발생한다.
- Problem 1 : No Mutual Exclusion (assume flag=0 to begin)
만약 위에 경우처럼 thread가 lock을 실행하고 flag를 1로 만들기 직전에 interrupt가 걸리는 경우가 계속 발생하면 여러 개의 thread가 임계 지점에 접근할 수 있게 된다.
- Problem 2 : Spin-waiting wates time waiting for another thread.
So, we need an atomic instruction supported by Hardware!
- test-and-set instruction, also known as atomic exchange
위와 같은 문제를 해결하기 위해서는 Hardware의 도움을 받아야한다.
Test And set(Atomic Exchange)
- An instruction to support the creation of simple locks
int TestAndSet(int *ptr, int new){
int old = *ptr;
*ptr = new;
return old;
}
- return(testing) old value pointed to by the ptr.
- Simultaneously update(setting) said value to new.
- This sequence of operations is perfomed atomically.
void lock(lock_t *lock){
while(TestAndSet(&lock->flag, 1) == 1)
; // spin-wait
}
TestAndSet이라는 코드를 만들어서 lock()의 while문 안에 넣어주게 되면 flag만 해주었을 때의 문제를 해결 할 수 있다.
그 이유는 현재 lock의 상태와 while문을 돌지말지를 한줄로 해결했기 때문이다.
Note : To work correctly on a single processor, it requires a preemptive scheduler. (while문을 계속 돌기 때문에)
- Correctness : yes
- The spin lock only allows a single thread to entry the critical section.
- Fairness : no
- Spin locks don't provide any fairness guarantees. Starvation can occur.
- Indeed, a thread spinning may spin forever. (preemptive scheduler가 없다면)
- Performance :
- In the single CPU, performance overheads can be quire painful.(계속 while문을 돌아서 그런 것 같다)
- If the number of threads roughly equals the number of CPUs, spin locks work reasonably well.
Compare-And-Swap (SPARC)
Test whether the value at the address(ptr) is equal to expected.
- If so, update the memory location pointed to by ptr with the new value.
- In either case, return the actual value at that memory location.
int CompareAndSwap(int *ptr, int expected, int new){
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}
Load-Linked and Store-Conditional(MIPS)
int LoadLinked(int *ptr){
return *ptr;
}
int StoreConditional(int *ptr, int value){
if( no one has updated *ptr since the LoadLinked to this address) {
*ptr = value;
return 1; // success!
} else {
return 0; // failed to update
}
}
- The store-conditional only succeeds if no intermittent store to the address has taken place.
- success : return 1 and update the value at ptr to value.
- fail : the value at ptr is not updates and 0 is returned.
아무도 ptr의 값을 update하지 않았으면 return 1아니면 return 0.
void lock(lock_t *lock){
while(1){
while(LoadLinked(&lock->flag)==1)
; //spin until it's zero
if(StoreConditional(&lock->flag,1)==1)
return; // if set-it-to-1 was a success: all done
otherwise : try it all over again
}
}
void unlock(lock_t *lock){
lock->flag=0;
}
Using LL/SC To Build a Lock
void lock(lock_t *lock){
while(LoadLinked(&lock->flag)||!StoreConditional(&lock->flag,1))
; //spin
}
A more concise form of the lock() using LL/SC
Fetch-And-Add
int FetchAndAdd(int *ptr){
int old = *ptr;
*ptr = old + 1;
return old;
}
- Atomically increment a value while returning the old value at a particular address.
ptr 값을 0과 1로만이 아니라 증가하도록 구현한다.
Ticket Lock
typedef struct __lock_t{
int ticket;
int turn;
} lock_t;
void lock_init(lock_t *lock){
lock->ticket = 0;
lock->turn = 0;
}
void lock(lock_t *lock){
int myturn = FetchAndAdd(&lock->ticket);
while(lock->turn!=myturn)
; //spin
}
void unlock(lock_t *lock){
FetchAndAdd(&lock->turn);
}
myturn = FetchAndAdd(&lock->ticket)에서 현재 ticket을 부여 받게 된다.
또, unlock을 하게되면 FetchAndAdd(&lock->turn)을 사용하여 lock->turn을 증가시키게 된다.
이렇게 구현을 하면 thread마다 고유의 ticket이 있으므로 ticket을 받은 순서대로 반드시 실행되게 된다. 따라서 fairness가 보장된다.
So Much Spinning
- Hardware-based spin locks are simple and they work.
- In some cases, these solutions can be quite inefficient.
- Any time a thread gets caught spinning, it wastes an entire time slice doing nothing but checking a value.
while문을 통해 check를 하게되면 해당 thread는 lock이 걸려 아무것도 안할 것이지만 자신이 호출된 time slice를 값을 체크하면서 모두 사용하게 된다. 이를 해결하기 위해서는 OS Support가 필요하다.
A Simple Approach : Just Yield
- When you are going to spin, give up the CPU to another thread.
- OS system call moves the caller from the running state to the ready state.
- The cost of a context switch can be substantial and the starvation problem still exist.
-
void lock(){ while(TestAndSet(&flag,1)==1) yield(); // give up the CPU }
그냥 lock이 걸려 있을 경우 yield (thread가 CPU를 포기)하는 방식으로 구현을 하면 while문을 돌지 않게 구현을 할 수 있으나, context switch에 많은 비용을 쓸 수도 있고, starvation problem이 해결되지 않는다.
'3-2 > 운영체제' 카테고리의 다른 글
Condition Variables (0) | 2021.11.15 |
---|---|
Lock-based Concurrent Data Structures (0) | 2021.11.13 |
Thread API (0) | 2021.11.08 |
Concurrency (0) | 2021.11.08 |
LRU Implementation in more detail (0) | 2021.11.08 |