Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

topcue

[OS] Locks 2 본문

Operating System

[OS] Locks 2

topcue 2021. 9. 3. 18:57

Too Much Spining

spin을 사용하는 정책의 고질적인 문제가 있다.

Bad Performance

한 스레드가 lock 상태에서 spin을 돌면 다른 스레드는 lock을 얻기 힘들어진다.

priority inversion

spin의 또 다른 문제점은 priority inversion이다. 이는 우선순위가 낮은 job이 먼저 스케줄 되는 상황을 말한다.

우선순위가 있는 스케줄링이라고 가정하자.

  • Priority inversion

이런 문제를 해결하는 방법 중 하나는 priority inheritance다.


A Simple Approach: Just Yield

이런 spin의 문제를 해결하기 위해 lock을 얻지 못하면 바로 Yield하는 방식을 사용할 수 있다.

  • Lock With Test-and-set And Yield
void init() {
	flag = 0;
}

void lock() {
	while (TestAndSet(&flag, 1) == 1)
		yield(); // give up the CPU
}

void unlock() {
	flag = 0;
}

yield를 하면 running에서 ready로 상태가 변한다. 즉 yield는 자기 스스로 deschedule하는 것이다.

  • Lock With Queues, Test-and-set, Yield, And Wakeup
typedef struct __lock_t {
	int flag;
	int guard;
	queue_t *q;
} lock_t;

void lock_init(lock_t *m) {
	m->flag  = 0;
	m->guard = 0;
	queue_init(m->q);
}

void lock(lock_t *m) {
	while (TestAndSet(&m->guard, 1) == 1)
		; //acquire guard lock by spinning
	if (m->flag == 0) {
		m->flag = 1; //
		m->guard = 0; }else{
		queue_add(m->q,
		m->guard = 0;
		park();
	}
}

void unlock(lock_t *m) {
	while (TestAndSet(&m->guard, 1) == 1)
		; //acquire guard lock by spinning
	if (queue_empty(m->q))
		m->flag = 0; // let go of lock; no one wants it
	else
		lock is acquired
	gettid());
	unpark(queue_remove(m->q)); // hold lock (for next thread!)
	m->guard = 0;
}
  • Spin vs Yield

하지만 yield는 context switcing을 하기 때문에 cost가 많이 들고, starvation 문제를 해결하지 못한다.

Using Queues: Sleeping

Spin 하는 대신 wait Q에서 sleep 하면서 기다린다.

Solaris는 park()로 스레드를 sleep시키고, unpark()로 스레드를 wake up 시킨다.

  • Lock With Queues, Test-and-set, Yield, And Wakeup (with guard)
typedef struct __lock_t {
	int flag;
	int guard;
	queue_t *q;  // waiting queue!
} lock_t;

void lock_init(lock_t *m) {
	m->flag  = 0;
	m->guard = 0;
	queue_init(m->q);
}

void lock(lock_t *m) {
	// ---------------- guard lock -------------------------
	while (TestAndSet(&m->guard, 1) == 1)
		; //acquire guard lock by spinning
	if (m->flag == 0) { // flag=0이면 1로
		m->flag = 1;      // lock is acquired
		// guard unlcok
		m->guard = 0;   // Test & Set 역할을 하는 작은 lock   											
	} else {            // flag=1이면 Q에서 기다린다.
			queue_add(m->q, gettid());
			m->guard = 0;   // guard 풀기
	// ---------------- guard unlock -----------------------
	// 여기서 다른 스레드가 unlock()을 해서 unpark()까지 하면 이 스레드를 다시는 못깨움
			park();        // sleep (Blocked)
	}
}

void unlock(lock_t *m) {
	// ---------------- guard lock -------------------------
	while (TestAndSet(&m->guard, 1) == 1)
		; //acquire guard lock by spinning
	if (queue_empty(m->q))
		m->flag = 0; // let go of lock; no one wants it
	else
		unpark(queue_remove(m->q)); // hold lock(for next thread)
		// 큐에서 대기(blocked)하던 스레드를 wake up(ready로 변화)

	// 위에서 flag는 풀었지만 아직 guard를 안풀었음.
	// 만약 여기서 다른 스레드의 lock()과 경쟁하면
	// 그 스레드는 guard에 의해 lock을 얻지 못함.
	m->guard = 0;
	// ---------------- guard unlock -----------------------
}

user-defined여서 크기를 알 수 없는 lock()과 unlock() 사이의 critical section을 spin 하는 대신 함수 내부에서 짧은 spin을 한다.

lock()을 하면 guard를 0으로 바꾸고 CPU를 yield 한다. while()에서 예상 가능한 시간만큼 spin한다.

race condition 가능성이 남아있다.

솔라리스는 setpark()을 사용하는데, setpark() 이후 unpark()이 실행되었는지 체크한다.

setpark() 이후 unpark()이 있으면 아래서 park를 안하고, unpark()이 없으면 나중에 park()한다.

  • setpark()
queue_add(m->q, gettid());
setpark(); // new code
m->guard = 0;


// 사용 예시
setpark()
unlock(guard)
// 여기에 inturrpt가 들어와서 다른 스레드가 unpark()할 수도 있음.
park() // <- unpark()이 있었으면 얘는 실행안함.
			 // unpark()이 없었으면 얘는 park()함. 

Different OS, Different Support (Futex)

리눅스는 futex를 사용한다.

  • Linux-based Futex Locks
/* 
 * mutex의 비트열에 대한 의미
 * 최상위 비트: lock을 가지고 있는지 나타낸다.
 * 나머지    : lock이 몇 개 걸렸는 지를 가지고 있다.
 */
void mutex_lock(int *mutex){
	int v;
	// 0->1: lock    /    1->1: lock 획득 실패
	if(atomic_bit_test_set(mutex, 31) == 0) // 최상위비트 test & set
		return;

	// 락 대기열에 하나가 더 들어갔음을 표기.
	atomic_increment(mutex);
	while(1) {  // 최상위 비트가 0이 될 때까지 반복
		// 0이면 1로 바꿈
		// 최상위 비트를 0->1로 바꾸었으면 if state
		// 원래 0이어서 0->0이면 else
		if(atomic_bit_test_set(mutex, 31) == 0){
			atomic_decrement(mutex);
			return;
		}
		// v는 최상위 비트를 점검하여 락 상태를 확인.
		// 최상위 비트가 1인 경우(v는 음수) lock이 걸렸음.
		v = *mutex; 
		if(v >= 0)
			continue;
		// goto sleep if (*mutex == v)
		futex_wait(mutex, v); // sleep
	}
}

void mutex_unlock(int *mutex){
	// 최상위 비트를 0으로 만들어서 unlock.
	// 최상위 비트에 +1해서 mutex가 0인지 확인
	if(atomic_add_zero(mutex, 0x80000000))
		return;
	futex_wake(mutex);
}

Spin vs Sleep

  • CPU 1개

    T2가 spin 하는 동안 T1이 스케줄 되지 않는다. 따라서 time out까지 기다려야 한다.

    만약 sleep을 한다면 context switch 정도의 시간 지연 후에 T1이 스케줄 될 수 있다.

  • CPU 2개

critical section이 context switching보다 긴 경우 sleep해서 다른 스레드에게 줄 수 있어서 유리하다.

critical section이 작은 경우 바로 스케줄되어서 실행할 수 있는 spin이 유리하다.

Two-Phase Locks

Two-Phase Lockspinsleep을 모두 사용하는 방식이다.

처음 phase에서는 먼저 lock을 얻기 위해 spin을 한다.

만약 처음 phase에서 lock을 얻지 못하면 sleep 한다.


'Operating System' 카테고리의 다른 글

[OS] Semaphores  (0) 2021.09.03
[OS] Condition Variables  (0) 2021.09.03
[OS] Lock-based Concurrent DS  (0) 2021.09.03
[OS] Locks 1  (2) 2021.09.03
[OS] Concurrency  (0) 2021.09.03
Comments