다양한 기록

멀티 스레드 / 경쟁 상태, 공유자원, 임계영역, 상호배제 본문

운영체제

멀티 스레드 / 경쟁 상태, 공유자원, 임계영역, 상호배제

라구넹 2024. 4. 10. 22:48

스레드: 제어의 흐름

프로세스: 제어의 흐름 + 수행을 하기 위한 자원

 

멀티 스레드 프로세스: 제어의 흐름들 + 수행을 하기 위한 자원

-> 자원의 공유가 발생, 경쟁. 컨커런시 제어 필요

 

컴퓨터 자원

- CPU(레지스터)

- 어드레스 스페이스 (코드 데이터 스탭 힙)

- 파일

 

어떻게 이런 자원을 관리하느냐

프로세스 모델

- 배타적으로 자원을 사용

- fork는 모든 자원을 다 새로 만듦

스레드 모델

- 필요한 자원들 중 몇몇만 공유, CPU나 스택은 따로 가짐

- 공유: 코드, 데이터, 힙, 파일

 

당연히 스레드가 더 빠르고, 공유하는 측면에서도 스레드가 좋음

-> 속도가 유리

 

프로세스는 한 프로세스가 죽어도 다른 프로세스에 영향을 안끼치지만,

스레드는 같은 프로세스 내 한 스레드가 죽으면 다 죽음

-> 스레드는 결함의 고립(isolation)에 불리

 

스레드의 장점

- 만들기 빠름 : 프로세스는 헤비 웨이트, 스레드는 라이트 웨이트

- 패럴리즘 : 소팅으로 예시를 들면 멀티스레드의 경우 분할 정복하면 됨 (맵 리듀스)

- 웨이팅 오버랩하기 쉬움 : 서버가 요청을 받으면 커넥션 만들고 스레드 만들어서 실행시키고 자기는 다른 요청 다시 기다리고..

- 데이터 쉐어링 쉬움

 

스케줄링의 대상으로서는 같은 걸로 여겨지지만(스케줄링 엔티티), 데이터 공유의 관점에서 다름

스레드는 TCB를 가지고, 프로세스처럼 상태전이도 하고, 자신만의 priority를 가질 수 있음

 

이러한 특징 상 스케줄링 정책 따라 스레드 중 뭐가 출력될 지가 알 수가 없고,

만들고 나면 컨트롤이 안됨

 

결과가 계속 다르고 정답도 아닌 결과가 나오기도 함(non-determistic) -> 타이밍 버그

 

타이머 인터럽트는 비동기적인 사건(어느 시점이든지 발생할 수 있음)

기계어 수준에서 스케줄링 되는 걸 보면 원자적으로 실행되지 않아 미싱이 발생하기도 함

 

해결책 -> 원자성

모든 걸 하거나, 안하거나. 

크리티컬 섹션(경쟁 상태를 야기하는 프로그램 코드) 부분은 원자적으로 처리되어야 함

#include<stdio.h>
#include<pthread.h>

static volatile int counter = 0;

void* mythread() {
	for(i = 0; i < 1e7; i++) {
    	counter = counter + 1;
    }
    return NULL;
}

int main(int argc, char *argv[]) {
	pthread_t p1, p2;
    pthread_create(&p1, NULL, mythread, NULL);
    pthread_create(&p2, NULL, mythread, NULL);
    
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    
    return 0;
}


1. 공유 자원

2. 임계 영역 (critical section)

3. 경쟁 상태 (race condition)

4. 상호 배제 (mutual exclusion)

 

위 코드에서 공유 자원은 전역변수 counter, 임계 영역은 mythread()의 counter = counter + 1이 될 것이고,

이 코드 앞 뒤로 락, 언락을 해주면 상호 배제가 될 것이다. 두 스레드가 카운터에 동시에 접근하려는 상태가 경쟁상태이다.

 

 

Concurrency의 두가지 이슈

1. 한 순간에 임계영역에 들어오는 스레드는 하나여야 한다

2. 동기화 : 한 스레드의 어느 부분은 항상 다른 스레드를 기다려야 한다

 

 

스레드 API

유저레벨 스레드: 2000년 초에 좀 쓰였고 요즘엔 안 씀

커널레벨 스레드

- 커널에서 스레드를 지원

- pthread, 리눅스 스레드, 자바스레드..

 

pthread

- 생성

- 대기

- 뮤텍스락, 언락

- 조건변수

 

* 스레드를 만들 때 어떤 값을 만들어서 리턴해야 하는 경우

=> 동적할당해서 넘겨야 함

리턴 값이 스택에 들어가면 스레드 끝나는 순간 날아가니까 주의

 

컨커런시 메커니즘

1. 락(lock)

// example
pthread_mutex_t lock;
int rc = pthread_mutex_init(&lock, NULL);
assert( rc == 0 );

pthread_mutex_lock(&lock);
x = x + 1;
pthread_mutex_unlock(&lock);

상호배제를 위함. 락과 언락의 사이는 원자성이 보장

 

2. 동기화 (synchronization)

// thread 1
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_mutex_lock(&lock);
while(ready == 0)
	pthread_cond_wait(&cond, &lock);
pthread_mutex_unlock(&lock);
// thread 2
pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);

wait와 signal을 하려면 우선 락을 해야 함. 조건 변수를 쓰기 전 락은 필수

스레드 둘 중 뭐가 먼저 실행될지는 몰라도, 스레드 2의 윗부분이 스레드 1의 밑부분보다 먼저 실행되는게 보장됨

순서 관계가 보장되는 경우

 

* 조건이 맞지 않으면 계속 웨이트..

그런데 락 걸고 웨이트 들어갔는데 어떻게 락이 또 걸리냐?

wait 들어가기 전에 락을 풀어주고, 깨어날때 다시 잠금

=> 데드락이 예방됨