nongdamgom 2024. 1. 15. 19:57

 

Building a Lock

  • low cost의 좋은 lock을 위해 hardware와 OS의 도움을 받을 수 있음

 

 

Evaluating locks - Basic criteria

1. Mutual exclusion

  • 여러개의 thread를 사용할 때, critical section을 보호할 수 있는지

2. Fairness(Starvation)

  • flag를 누가 catch 했냐에 따라 lock을 잡고 못잡고가 결정 => 운이 안좋으면 계속 못잡을 수도 있음

3. Performance

  • spin waiting을 하면서, 계속 다른 thread가 기다려야 하는 문제 => cpu 낭비

==> 이러한 문제들을 어떻게 해결할 수 있을까?

 

 

Controlling Interrupts

void lock(){
	DisableInterrupts();
}

void unlock(){
	EnableInterrupts();
}

 

  • Disable Interrupts
  • 예전에 사용하던 방식
  • cpu가 하나일 때, 한 process가 lock을 잡으면 context switching을 막아서 critical section 보호
  • Problem
  • greedy한 program이 cpu를 독점할 수 있음
  • multiprocessors 에서는 작동 x => interrupt를 막는건, 해당 core(cpu)에서만 막는거라서, 다른 core로 가면 막을 수 x
  • cost가 많이 든다 => slowly

 

 

Why hardware support needed?

  • 26.  Concurrency 할 때 봄
  • First attempt : flag 사용 
  • 중간에 interrupt가 오면 둘다 자기가 lock을 잡고 있다고 생각하는 문제 발생
  • spin waiting 문제 

==> test and set instruction 사용

 

 

 

Test And Set (Atomic Exchange)

int TestAndSet(int *ptr, int new){
    int old = *ptr;  // old value에 *ptr(flag)을 저장
    *ptr = new;  // *ptr(flag)에 new 값 저장
    return old; // old value 반환
}
  • 결론적으로 old value 반환 == flag에 담긴 값 반환 (flag가 0이면 0 반환, 1이면 1 반환)
  • 이런 Test and Set이라는 hardware적 명령어가 1개 있는거(위는 c코드지만 진짜 c코드는 아님 그냥 예시)
  • First attempt 때, interrupt가 중간에 오는 문제를, 이러한 hardware 명령어 1개를 사용하게 되면, 이 instruction이 다 끝날 때까지 interrupt를 못받음(만약 온다면 반려 후, 이 instruction이 끝난 후 실행)
  • 즉, 중간에 interrupt가 와도 문제 x ==> performed atomically

 

 

A Simple Spin lock using test-and-set

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *lock){
	lock->flag = 0;
}

void lock(lock_t *lock){
	while(TestAndSet(&lock->flag, 1) == 1)  // 1이면 대기
    	;  // spin - wait
}

void unlock(lock_t *lock){
	lock->flag = 0;  //unlock 후 다시 0으로 만들어서 대기 풀어줌
}
  • core 0번과 core 1번에서 T-A-S를 동시 수행 한다고 해도, hardware에서 동시 실행 안되게 직렬적으로 만들어줌
  • NOTE : single processor 에서 맞게 작동하려면 preemptive scheduler 여야 함
  • => process A가 critical section 수행 중 yield()를 호출하면, process B가 수행 되는데, 이 때 lock에 대한 제어권은 A가 계속 가지고 있기 때문에, B는 수행이 안되고 flag 1 에서 무한 loop가 돌게 됨 
  • => 즉 OS가 다시 제어권을 잡을 수 없기 때문에, preemptive한 scheduler 여야 하는 것

 

 

Evaluating Spin Locks

1. Correctness : yes 

  • critical section을 보호할 수 있음

2. Fairness : no

  • 어떤 fairness도 보장하지 않음

3. Performance : 해결 x

 

 

 

Fetch-And-Add(Hardware 명령어)

int FetchAndAdd(int *ptr){
    int old = *ptr;
    *ptr = old + 1;
    return old;
}

 

 

 

Ticket Lock

  • fetch-and-add를 이용해서 만든 lock
  • 도착한 순서대로 실행되는 것을 fair하다고 했을 때, 이 lock은 fairness 하다고 말 할 수 있음
typedef sturct __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);
    // myturn에 old value, ticket은 + 1 됨
    while(lock->turn != myturn)
    	;  //spin
}

void unlock(lock_t *lock){
	FetchAndAdd(&lock->turn);
}
  • 처음에 ticket이랑 turn 전부 0으로 초기화
  • 처음에 lock 부르면 turn = 0, ticket = 1 => 이 thread가  unlock 할 때 turn + 1 됨
  • 다음 thread가 lock을 부르면 turn = 1, ticket = 2 를 받음
  • 처음 thread unlock을 해서 turn 이 1이 될 때까지 두번째 thread는 대기, 1되면 lock 잡음 
  • 이런 식으로 흘러나간다. ==> 순서대로 실행 될 수밖에 없음

 

 

So Much Spinning

  • Correctness와 fairness는 해결이 됐지만 performance는 여전히 spinning 문제 때문에 안좋다

==> Spinning을 피하려면 OS의 도움을 받아야함

 

** hardware의 도움을 받아서 mutual exclusion 과 fairness 문제 해결, 해결되지 않는 performance 문제를 OS 도움을 받아서 해결하겠다...........

 

 

 

 

A Simple Approach : Just Yield

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

 

  •  spinning에 들어가면 yield()를 불러서 cpu를 다른 thread에 넘겨줌
  • yield()를 부른 thread는 run queue의 맨 뒤로 들어가서 대기 

==> yield() 하면서 context switch 하는 cost와 starvation 문제가 여전히 존재 

 

 

 

Using Queues : Sleeping Instead of Spinning

  • 위의 문제를 해결하기 위해 도입 
  • park() : sleep 과 비슷함, 스스로 잠에 든다
  • unpark(threadID) : wake up과 비슷, 남이 나를 깨워줌
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)
    	; // queue에 대한 lock을 먼저얻음
        // queue를 조작하는 시간은 짧으므로 spinning 문제 해결 가능
    if(m->flag == 0){ 
    // 위에서 이미 guard로 lock을 잡았기 때문에, 여기선 T-A-S 쓸 필요 x
    	m->flag = 1;  // critical section lock 얻고
        m->guard = 0;  // queue에 대한 lock은 놔줌
    }
    else{  // queue에 대한 lock은 잡았는데, critical section을 다른 thread가 쓰고 있음
    	queue_add(m->q, gettid());  // queue에 내 tid 추가
        m->guard = 0;  // 이미 id 넣었으니 더이상 lock 필요 x => 놔줌
        park(); // 잠든다
    }
}

void unlock(lock_t *m){
    while(TestAndSet(&m->guard, 1) == 1)
    	;  // unlock을 하면 queue에서 다른 thread 같이 깨워줘야 돼서 , queue를 수정할 수 있을 때만 진행
    if(queue_empty(m->q)) // queue에 대기 중인 thread가 없으면
    	m->flag = 0; // lock 바로 놔줌
    else
    	unpark(queue_remove(m->q)); // queue에서 지우고 첫번째 thread 꺼내줌 
        // 여기서 나온 thread는 바로 critical section 수행 가능
        // 왜냐면 flag를 0으로 만들지 않았기 때문에 그냥 계속 flag는 1로 유지
        //(뒤에 대기 중인 애가 없을 때까지)
    m->guard = 0; // queue lock을 놓음 
}
  • flag => critical section의 lock
  • guard => queue의 lock