[운영체제] 6. 스레드 동기화

황재성·2022년 5월 30일
0

운영체제

목록 보기
6/9

스레드 동기화의 필요성

스레드 동기화의 필요성

  • 다수의 스레드가 동시에 공유 데이터에 접근하면
    - 공유 데이터가 훼손되는 문제 발생
  • 스레드 동기화
    - 공유 데이터에 대한 다수의 스레드가 동시에 접근할 때 공유데이터가 훼손되는 문제의 해결책
    - 공유데이터를 접근하고자 하는 다수의 스레드가 충돌없이 공유데이터에 접근하기 위해 상호 협력하는 것

공유 데이터 접근 문제의 해결책

  • 문제점
    - 여러 스레드가 공유 변수에 접근할 때, 공유데이터 훼손
  • 해결책 : 스레드 동기화
    - 한 스레드가 공유데이터에 대한 접근을 마칠 때까지
    - 다른 스레드가 공유 데이터를 접근하지 못하도록 제어
  • 멀티스레드의 경쟁 상황이 자주 발생하는가?
    - 매우 자주 발생
    - 커널 코드에서 자주 발생 (커널에 공유데이터가 많기 때문)
    - 다중 코어에서 더욱 조심

임계구역과 상호배제

  • 스레드 동기화와 관련된 2가지 중요 개념 : 임계구역과 상호배제
  • 임계구역
    - 공유데이터에 접근하는 프로그램 코드들
  • 상호배제
    - 임계구역이 오직 한 스레드만 배타독점적으로 사용되도록 하는 기술
    1) 임계구역에 먼저 진입한 스레드가 임계구역의 실행을 끝낼 때까지
    2) 다른 스레드가 진입하지 못하도록 보장
      

상호배제

상호배제를 포함하는 프로그램

  1. 일반코드(non-critical code)
  • 공유데이터를 액세스하지 않는 코드
  1. 임계구역 진입 코드(entry code)
  • 상호배제를 위해 필요한 코드
    - 임계구역에 진입하기 전 필요한 코드 블록
  • 현재 임계구역을 실행 중인 스레드가 있는지 검사
    - 없다면, 다른 스레드가 들어오지 못하도록 조치
    - 있다면, 진입이 가능해질 때까지 대기
  1. 임계구역 코드(critical code)
  2. 임계구역 진출 코드(exit code)
  • 상호배제를 위해 필요한 코드
    - 임계구역의 실행을 마칠 때 실행되어야 하는 코드 블록
  • entry code에서 대기중인 스레드가 임계구역에 진입할 수 있도록 entry code 에서 취한 조치를 해제하는 코드

상호배제 구현

  • 상호배제 구현 목표
    - 임계구역에 오직 1개의 스레드만 진입
  • 상호배제 구현 방법
    - 소프트웨어적 방법 - Peterson's 알고리즘 등
    - 하드웨어적 방법 - 인터럽트 서비스 금지, 원자 명령 활용 (오늘날 대부분 하드웨어 솔루션 사용)
  • 하드웨어적 방법
  • 임계 구역 진입/진출 코드에 구현
  • 방법1 - 인터럽트 서비스 금지
    - 인터럽트가 발생해도 CPU가 인터럽트를 무시하도록 하는 CPU 명령 이용
  • 방법2 - 원자 기계 명령 사용
    - 상호배제 구현에 가장 많이 사용하는 방법
    - 임계구역에 진입할 때, 임계구역을 잠그고 들어가는 명령 하나(원자명령)로 다른 스레드가 들어오지 못하게 하는 방법

상호배제 구현 방법1 - 인터럽트 서비스 금지

1) 인터럽트 서비스 금지 방법

  • 임계구역 entry 코드에서 인터럽트 서비스를 금지하는 명령 실행

    cli ; entry 코드. 인터럽트 서비스 금지 명령 cli (clear interrupt flag)
    ...
    임계구역 코드
    ...
    sti ; exit 코드. 인터럽트 서비스 명령 허용 sti (set interrupt flag)

1) 장치로부터 인터럽트가 발생해도, CPU가 인터럽트 발생을 무시
2) 인터럽트가 발생해도 CPU는 인터럽트 서비스 루틴을 실행하지 않음
3) 인터럽트를 무시하면 임계구역을 실행하는 스레드가 중단되지 않음

2) 문제점

  • 모든 인터럽트가 무시되는 문제 발생
  • 멀티코어 CPU나 다중 CPU를 가진 시스템에서 활용 불가
    - 한 CPU의 인터럽트 금지로 다른 CPU에게 까지 인터럽트 금지는 불가하다.
    • 해결 방법 : CPU는 lock 접두어가 붙은 명령을 처리할 때, CPU의 LOCK핀에 신호를 발생시켜 현재 액세스하고 있는 메모리에 다른 프로세서들이 접근하지 않도록 한다. 그러므로 컴퓨터 설계자는 LOCK 신호를 이용하여 다른 프로세서의 공유 메모리 접근을 막도록 회로를 구성하여야 한다.

단순 lock변수로 상호배제 시도

  • locking/unlocking 방식으로 임계구역의 entry/exit 코드 작성하면 상호배제가 가능할까?
    - lock 변수 : 1이면 잠금상태
    - lock 변수 : 0이면 열린상태

상호배제구현 방법2 - 원자명령 사용

  • lock 변수를 이용한 상호배제의 실패 원인
    - 실패 원인은 entry code에 있음
    - lock 변수 값을 읽는 명령과 lock 변수에 1을 저장하는 2개의 명령 사이에 컨텍스트 스위칭이 될 때 문제 발생
  • 해결책 - 원자 명령 도입
    - lock 변수를 읽어들이는 명령과 lock 변수에 1을 저장하는 2개의 명령을 한 번에 처리하는 원자 명령 필요
    - 원자 명령 : TSL (Test and Set Lock)
    • 1970년대 Intel Pentium에서 시작. 대부분의 CPU에서 제공

멀티스레드 동기화 기법

멀티스레드 동기화

1) 멀티스레드 동기화란?

  • 상호배제 기반위에
  • 자원을 사용하려는 여러 스레드들이 자원을 원활히 공유할 수 있도록 하는 기법
  • 동기화 프리미티브(synchronization primitives)로 부름

2) 대표적인 기법

  • lock 방식 : 뮤텍스(mutex), 스핀락(spilock)
    - 상호배제가 되도록 만들어진 락(lock)활용
    - 락을 소유한 스레드만이 임계구역 진입
    - 락을 소유하지 않은 스레드는 락이 풀릴 때까지 대기

  • wait-signal 방식 : 세마포(semaphore)
    - n개의 자원을 사용하려는 m개 멀티스레드의 원활한 관리
    - 자원을 소유하지 못한 스레드는 대기(wait)
    - 자원을 다 사용한 스레드는 알림(signal)

뮤텍스

  • mutual exclusion 의 약자이다.

1) 뮤텍스

  • 잠김/열림 중 한 상태를 가지는 lock 변수 이용
  • 한 스레드만 임계구역에 진입시킴
  • 다른 스레드는 큐에 대기
  • sleep-waiting lock 기법

2) 구성요소
1. 락 변수

  • true/false 중 한 값
  • true : 락을 잠근다. 락을 소유한다.
  • false : 락을 연다. 락을 해제한다.
  1. 대기 큐
  • 락이 열리기를 기다리는 스레드 큐
  1. 연산
  • lock 연산(임계구역은 entry code)
  • 락이 잠김 상태(lock = ture) 이면, 현재 스레드를 블록 상태로 만들고 대기 큐에 삽입
  • 락이 열린 상태이면, 락을 잠그고 임계구역 진입

뮤텍스의 특징

1) 뮤텍스를 이용한 동기화 특징

  • 임계구역의 실행시간이 짧은 경우, 비효율적
    - 락이 잠겨 있으면(컨택스트 스위칭되어) 대기 큐에서 대기, 락이 풀리면 다시(컨택스트 스위칭되어) 실행 -> 락이 잠겨 있는 시간보다 스레드가 잠자고 깨는데 걸리는 시간이 상대적으로 크기 때문

2) 뮤텍스 동기화를 위한 POSIX 표준 라이브러리

  • 뮤텍스락 변수
    - pthread_mutex_t lock;
  • 뮤텍스 조작 함수들
    - pthread_mutex_init() - 뮤텍스락 변수 초기화
    - pthread_mutex_lock() - 뮤텍스락 잠그기
    - pthread_mutex_unlock() - 뮤텍스락 풀기
    - pthread_mutex_destroy() - 뮤텍스락 변수 사용 종료

3) pthread를 이용한 뮤텍스 동기화 코딩 사례

pthread_mutex_t lock;				//뮤텍스락 변수 생성
pthread_mutex_init(&lock, NULL);	//뮤텍스락 변수 초기화
pthread_mutex_lock(&lock);			//임계구역 entry 코드. 뮤텍스락 잠그기

...임계구역 코드...

pthread_mutex_unlock(&lock);		//임계구역 exit 코드. 뮤텍스락 열기

pthread의 뮤텍스를 이용한 공유집계판의 스레드 동기화

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

int sum = 0;
pthread_mutext_t lock;

void* worker(void arg){
	printf("%s 시작 \t %d\n", (char*)arg, sum);
    
    for(int i = 0; i < 1000000 ; i++){
    	pthread_mutex_lock(&lock);
        sum = sum + 10;
        pthread_mutex_unlock(&lock);
    }
    printf("%s 끝 \t %d\n", (char*)arg, sum);
}

int main(){
	char *name[] = {"황기태", "이찬수"};
    pthread_t tid[2];
    pthread_attr_t attr[2]; //스레드 정보를 담을 구조체
    
    pthread_attr_init(&attr[0]);
    pthread_attr_init(&attr[1]);
    
    pthread_mutex_init(&lock, NULL);
    
    pthread_create(&tid[0], &attr[0], worker, name[0]);
    pthread_create(&tid[1], &attr[1], worker, name[1]);
    
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    
    printf("최종 sum = %d\n", sum);
    
    pthread_mutex_destroy(&lock);
    
    return 0;
}

스핀락

1) 스핀락

  • busy-waiting lock 기법
    - 스레드가 큐에서 대기하지 않고 락이 열릴 때까지 계속 락 변수 검사
  • 뮤텍스와 거의 같고 busy-waiting 이라는 점에서만 다름
    - 대기큐 없음
    - busy-waiting 으로 인해 CPU를 계속 소모. CPU가 다른 스레드를 실행시킬 수 없음
  • lock을 소유한 스레드만 자원 배타적 사용하는 동기화 기법
    - 공유자원 하나 당 하나의 스핀락 사용

2) 구성요소
1. 락 변수
- true/false 중 한 값
- true : 락을 잠근다. 락을 소유한다.
- false : 락을 연다. 락을 해제한다.

  1. 연산
  • lock 연산
    - 임계구역에 들어갈 때 실행되는 entry code
    - 락이 잠김 상태면, 락이 풀릴 때까지 무한 루프 돌면서 lock 연산 시도
    - 락이 열린 상태면, 락을 잠김 상태로 바꾸고 임계구역 실행
  • unlock 연산
    - 임계구역을 나올 때 실행하는 exit code
    - 락을 열림 상태로 변경

스핀락 특징

1) 스핀락을 이용한 동기화 특징

  • 뮤텍스의 non-blocking 모델
    - 락이 잠겨 있을 때 블록되지 않고 락이 풀릴 때까지 검사 코드 실행
  • 단일 CPU를 가진 운영체제에서 비효율적, 멀티코어에 적합
    - 단일 코어 CPU에서 의미 없는 CPU 시간 낭비 (Lock을 갖고 있는 스레드를 풀어주려면 단일 코어 CPU에서는 어차피 컨택스트 스위칭을 해야하기 때문이다)
    -> 스핀락을 검사하는 스레드의 타임 슬라이스가 끝날 때까지 다른 스레드는 실행 안 됨. 다른 스레드의 실행 기회 뺏음
    -> 락을 소유한 다른 스레드가 실행되어야 락이 풀림
    - 임계구역의 실행 시간이 짧은 경우 효과적

2) 스핀락 동기화를 위한 POSIX 표준 라이브러리

  • 스핀락 변수
    - pthread_spin_t lock;
  • 스핀락 조작 함수들
    - pthread_spin_init() - 스핀락 변수 초기화
    - pthread_spin_lock() - 스핀락 잠그기
    - pthread_spin_unlock() - 스핀락 풀기
    - pthread_spin_destroy() - 스핀락 변수 사용 종료

3) pthread를 이용한 스핀락 동기화 코딩 사례

pthread_spinlock_t lock;		//스핀락 변수 생성
pthread_spin_init(&lock, NULL);	//스핀락 변수 초기화
pthread_spin_lock(&lock);		//임계구역 entry code. 스핀락 잠그기

... 임계구역 코드 ...

pthread_spin_unlock(&lock);		//임계구역 exit code. 스핀락 열기 

pthread의 스핀락을 이용한 공유 집계판의 스레드 동기화

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

int sum = 0;
pthread_spinlock_t lock;

void* worker(void arg){
	printf("%s 시작 \t %d\n", (char*)arg, sum);
    
    for(int i = 0; i < 1000000 ; i++){
    	pthread_spin_lock(&lock);
        sum = sum + 10;
        pthread_spin_unlock(&lock);
    }
    printf("%s 끝 \t %d\n", (char*)arg, sum);
}

int main(){
	char *name[] = {"황기태", "이찬수"};
    pthread_t tid[2];
    pthread_attr_t attr[2]; //스레드 정보를 담을 구조체
    
    pthread_attr_init(&attr[0]);
    pthread_attr_init(&attr[1]);
    
    pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
    //lock을 한 프로세스에 속한 스레드만이 공유하는 변수로 선언
    
    pthread_create(&tid[0], &attr[0], worker, name[0]);
    pthread_create(&tid[1], &attr[1], worker, name[1]);
    
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    
    printf("최종 sum = %d\n", sum);
    
    pthread_mutex_destroy(&lock);
    
    return 0;
}

뮤텍스와 스핀락은 어떤 경우에 적합한가?

  1. 락이 잠기는 시간이 긴 응용 : 뮤텍스
  • 락을 얻지 못했을 때, CPU를 다른 스레드에게 양보하는 것이 효율적
  • 락이 잠기는 시간이 짧은 경우 : 스핀락이 효율적
  1. 단일 CPU를 가진 시스템 : 뮤텍스
  • 단일 CPU에서 스핀락은 크게 의미 없음
  1. 멀티 코어(멀티 CPU)를 가진 시스템 : 스핀락
  • 잠자고 깨는 컨택스트 스위칭 없이 바로 자원 사용
  • 임계구역은 가능한 짧게 작성하므로
  1. 사용자 응용프로그램 : 뮤텍스, 커널 코드 : 스핀락
  • 커널 코드나 인터럽트 서비스 루틴은 빨리 실행되어야 하고, 인터럽트 서비스 루틴 내에서 잠잘 수 없기 때문
  1. 스핀락을 사용하면 기아 발생 가능
  • 스핀락은 무한 경쟁 방식이어서 기아가 발생 가능
    - 락을 소유한 스레드가 락을 풀지 않고 계속 실행하거나 종료해버린 경우, 코딩이 잘못된 경우

뮤텍스와 스핀락 비교

뮤텍스스핀락
대기큐있음없음
블록 가능 여부락이 작겨 있으면 블록됨락이 잠겨 있어도 블록되지 않고 계속 락 검사
lock/unlock 연산 비용저비용CPU를 계속 사용하므로 고비용
하드웨어 관련단일 CPU에서 적합멀티코어 CPU에서 적합
주 사용처사용자 응용 프로그램커널 코드, 인터럽트 서비스 루틴

왜 알아야 하는가?

  • 개발자로서 둘 중 하나를 선택하여야하고, 시스템의 성능 관점에서 볼 수 있어야 하기 때문이다.

세마포

  • 세마포가 필요한 상황

1) 세마포의 정의

  • 멀티스레드 사이의 자언 관리 기법
    - n개의 공유 자원을 다수 스레드가 공유하여 사용하도록 돕는 자원 관리 기법 (n개의 프린터가 있는 경우, 프린터를 사용하고자 하는 다수 스레드의 프린터 관리)

2) 구성요소
1. 자원 : n개
2. 대기 큐 : 자원을 할당 받지 못한 스레드들이 대기하는 큐
3. counter 변수

  • 사용 가능한 자원의 개수를 나타내는 정수형 전역 변수
  • n으로 초기화 (counter = n)
  1. P/V 연산
  • P연산(wait 연산) - 자원 요청 시 실행하는 연산 (자원 사용 허가를 얻는 과정)
  • V연산(signal 연산) - 자원 반환 시 실행하는 연산 (자원 사용이 끝났음을 알리는 연산)

P연산과 V연산

  • P/V를 wait/signal 로 표기하기도 함
    - P연산 : 자원 사용을 허가하는 과정, 사용 가능 자원 수 1 감소(counter--)
    - V연산 : 자원 사용을 마치는 과정, 사용가능 자원 수 1 증가(counter++)
  • 세마포 종류 2가지 - 자원을 할당 받지 못한 경우의 행동에 따라 구분
    1) sleep-wait 세마포
  • P연산 : 대기 큐에서 잠자기, V연산 : 사용가능 자원이 있으면 잠자는 스레드 깨우기
P 연산 { 
	counter--;
    if counter < 0 {
    ... 현재 스레드들 대기 큐에 삽입 ... 
    }
    ... 자원 획득 ...
}
V 연산 {
	counter++;
    if counter <= 0 {
    ... 대기 큐에서 한 스레드 깨움 ...
    }
}

2) busy-wait 세마포

  • P연산 : 사용 가능 자원이 생길 때까지 무한 루프, V연산 : counter--;
P 연산 {
	while counter <= 0;
    counter--;
}
V 연산 { 
	counter++;
}

세마포 활용을 위한 POSIX 표준 라이브러리

1) 세마포 구조체

  • sem_t s; // counter 변수 등을 가진 세마포 구조체

2) 세마포 조작 함수들

  • sem_init() - 세마포 초기화
  • sem_destroy() - 세마포 기능 소멸
  • sem_wait()
    - P연산을 수행하는 함수 (blocking call)
    - sleep-wait 방식으로, 가용 자원이 없으면 대기 큐에서 잠을 잠
  • sem_trywait()
    - p연산을 수행하는 함수(non-blocking call)
    - 가용 자원이 있으면, counter 값을 감소시키고 0 리턴
    - 없으면, counter 값을 감소시키지 않고 -1 리턴
  • sem_post() - V연산을 수행하는 함수
  • sem_getvalue() - 세마포의 현재 counter 값을 리턴하는 함수

세마포 활용 사례

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

sem_t toiletsem; // POSIX 세마포 구조체로 모든 스레드에 의해 공유

void* guestThread(void* arg){ // 고객의 행동을 묘사하는 스레드 코드
	int cnt = -1;
    
    sem_wait(&toiletsem); // P연산. 자원 사용 요청. 세마포의 counter 값 1 감소
    sem_getvalue(&toiletsem, &cnt); // 세마포의 counter 을 cnt 변수로 읽어오기
    printf("고객%s 화장실에 들어간다.. 세마포 conter = %d\n" ,(char*)arg, cnt); // 1초동안 화장실을 사용한다. 
    sem_post(&toiletsem); // V연산. 화장실 사용을 끝냈을음 알림
   	sem_getvalue(&toiletsem, &cnt); // 세마포의 counter 값을 cnt 변수로 읽어오기
    printf("고객%s 화장실에서 나온다. 세마포 counter = %d\n", (char*)arg, cnt);
}

#define NO 0 // 자식 프로세스와 세마포 공유하지 않음
#define MAX_COUNTER 3 // 자원의 개수, 동시에 들어갈 수 있는 스레드의 개수

int main(){
	int counter = -1;
    char *name[] = {"1", "2", "3", "4", "5"};
    pthread_t t[5]; // 스레드 구조체
    
    //세마포 초기화 : MAX_COUNTER 명이 동시에 사용
    sem_init(&toiletsem, &counter);
    sem_getvalue(&toiletsem, &counter); // 세마포의 현재 counter 값 읽기
    printf("세마포 counter = %d\n", counter);
    
    for(int i = 0; i < 5; i++) pthread_create(&t[i], NULL, guestThread, (void*)name[i]); // 5명의 고객 스레드 생성
    
    for(int i = 0; i< 5; i++) pthread_join(t[i], NULL); // 모든 고객이 소멸할 때까지 대기
    
    sem_getvalue(&toiletsem, &counter); // 세마포의 현재 counter 값 읽기
    
    printf("세마포 counter = %d\n", counter);
    sem_destroy(&toiletsem); // 세마포 기능 소멸
    
    return 0;
}

-> 3개의 칸이 있는 화장실을 5명의 고객이 사용하고자 할 때 세마포를 이용하여 3칸의 화장실을 5명의 고객 스레드가 활용할 수 있게 관리하는 예시

카운터 세마포와 이진 세마포

1) 카운터 세마포

  • 자원의 인스턴스가 여러 개인 경우 (앞서 설명)
    2) 이진 세마포
  • 자원이 1개 있는 경우 멀티스레드 사이의 자원 관리
  • 1개의 자원에 대해 1개의 스레드만이 액세스할 수 있도록 보호
    - 뮤텍스와 매우 유사

3) 이진 세마포의 구성 요소
1. 세마포 변수 S

  • 0과 1중 하나를 가지는 전역 변수, S는 1로 초기화
  1. 대기큐
  • 사용 가능한 자원이 생길 때까지 스레드들이 대기하는 큐
  • 스레드 스케줄링 알고리즘 필요
  1. 2개의 원자 연산
  • wait 연산(P연산) - 자원 사용 허가를 얻는 과정
    - S가 1 감소 시키고, 0보다 작으면 대기 큐에서 잠듬 0보다 크거나 같으면 자원 사용하는 코드 실행
  • signal(V연산) - 자원 사용이 끝났음을 알리는 과정
    - S를 1 증가시키고, 0보다 크면 그냥 리턴, 0보다 작거나 같으면 대기 큐에 있는 스레드 중 한 개를 깨움

동기화 이슈 : 우선순위 역전

  • 우선 순위 역전(priority inversion)
    - 스레드의 동기화로 인해 높은 순위의 스레드가 낮은 스레드보다 늦게 스케줄링 되는 현상 -> 우선순위를 기반으로 스케줄링하는 실시간 시스템에서 스레드 동기화로 인해 발생
  • 우선 순위 역전의 문제점
    - 실시간 시스템의 근본 붕괴
    1) 우선 순위가 높다는 것은 중요한 일을 할 가능성, 높은 순위의 스레드가 늦게 실행되면 심각한 문제 발생 가능
    2) 낮은 순위의 스레드가 길어지면 더욱 더 심각한 문제 발생

우선순위 역전 사례

우선순위 역전 해결책

1) 우선순위 올림(priority ceiling)

  • 스레드가 공유 자원을 소유하게 될 때, 스레드의 우선순위를 미리 정해진 높은 우선순위로 일시적으로 올림
  • 선점되지 않고 빨리 실행되도록 유도

2) 우선순위 상속(priority inheritance)

  • 낮은 순위의 스레드가 공유 자원을 가지고 있는 동안
  • 높은 순위의 스레드가 공유 자원을 요청하면
  • 공유 자원을 가진 스레드의 우선순위를 요청한 스레드보다 높게 설정하여 빨리 실행시킴

생산자 소비문제

응용프로그램에 존재하는 생산자 소비자 문제 사례

  • 생산자 소비자 문제는 많은 응용프로그램에서 발생하는 전형적인 동기화 문제

생산자 소비자 문제의 정의

1) 생산자 소비자 문제란?

  • 공유버퍼를 사이에 두고, 공유버퍼에 데이터를 공급하는 생산자들과
  • 공유버퍼에서 데이터를 읽고 소비하는 소비자들이 공유 버퍼를 사용할 때
  • 공유버퍼를 문제 없이 사용하도록 생산자와 소비자를 동기화시키는 문제
  • 멀티스레딩 응용프로그램 작성 시 자주 발생

2) 생산자 소비자 문제를 코딩할 때 구체적으로 해결해야하는 3가지 문제

  • 문제1
    - 상호 배제 해결
    - 생산자들과 소비자들의 공유 버퍼에 대한 상호 배제
  • 문제2
    - 비어 있는 공유 버퍼 문제(비어 있는 공유버퍼를 소비자가 읽을 때)
  • 문제3
    - 꽉 찬 공유버퍼 문제(꽉 찬 공유버퍼에 생산자가 쓸 때)

비어 있는 공유버퍼 문제 해결

  • 세마포 R 활용(읽기 가능한 버퍼 개수) : 버퍼가 비어 있는지 살피는 P/V연산으로 해결

꽉 찬 공유버퍼 문제 해결

  • 세마포 W(쓰기 가능한 버퍼 개수) 활용 : 버퍼가 꽉 차 있을 때 처리하는 P/V연산으로 해결

생산자와 소비자 알고리즘

생산자-소비자로 구성된 응용프로그램 만들기

  • 생산자 스레드
    - 0~9까지 10개의 정수를, 랜덤한 시간 간격으로, 공유버퍼에 쓴다.
  • 소비자 스레드
    - 공유버퍼로부터 랜덤한 시간 간격으로, 10개의 정수를 읽어 출력한다.
  • 공유버퍼
    - 4개의 정수를 저장하는 원형 큐로 작성
    - 원형 큐는 배열로 작성
  • 2개의 세마포 사용
    - semWrite : 공유버퍼에 쓰기 가능한 공간(빈 공간)의 개수를 나타냄(초기값이 4인 counter 소유)
    - semRead : 공유버퍼에 읽기 가능한 공간(값이 들어 있는 공간)의 개수를 나타냄(초기값이 0인 counter 소유)
  • 1개의 뮤텍스 사용
    - pthread_mutex_t critical_section
    - 공유버퍼에서 읽는 코드와 쓰는 코드를 임계구역으로 설정
    - 뮤텍스를 이용하여 상호배제
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <stdlib.h>

#define N_COUNTER 4 //공유 버퍼에 저장할 정수 공간의 개수
#define MILLI 1000

void mywrite(int n);
int myread();

pthread_mutex_t critical_section;
sem_t semWrite, semRead; //POSIX 세마포
int queue[N_COUNTER]; //공유버퍼
int wptr; //queue[]에 저장할 다음 인덱스
int rptr; //queue[]에서 읽을 다음 인덱스

void* producer(void* arg){ //생산자 스레드 함수
	for(int i = 0; i<10; i++){
    	mywrite(i); //정수 i를 공유버퍼에 저장
        printf("producer : wrote %d\n", i); 
        
        //m 밀리초 동안 잠을 잔다.
        int m = rand()%10; //0~9 사이의 랜덤한 정수
        usleep(MILLI*m*10); //m*10 밀리초동안 잠자기
    }
    return NULL;
    
}

void* consumer(void* arg){ //소비자 스레드 함수
	for(int i =0; i<10; i++){
    	int n = myread(); //공유버퍼의 맨 앞에 있는 정수 읽어 리턴
        printf("\tconsumer : read %\n", i);
        
        
        //m 밀리초동안 잠을 잔다
        int m = rand()%10; //0~9 사이의 랜덤한 정수
        usleep(MILLI*m*10); //m*10 밀리초동안 잠자기
    }
    return NULL;
}

void mywrite(int n){ //정수 n을 queue[]에 삽입
	sem_wait(&semWrite); //queue[]에 쓸 수 있는지 요청
    
    pthread_mutex_lock(&critical_section); //뮤텍스 락 잠그기
    queue[wptr] = n; //버퍼에 정수 n을 삽입
    wptr++;
    wptr% = N_COUNTER;
    pthread_mutex_unlock(&critical_section); //뮤텍스 락 열기
}

int myread() { //queue[] 맨 앞에 있는 정수를 읽어 리턴
	sem_wait(&semRead); //queue[]에서 읽을 수 있는지 요청
    
    pthread_mutex_lock(&critical_section); //뮤텍스 락 잠그기
    int n = queue[rptr]; //버퍼에서 정수를 읽는다.
    rptr++;
    rptr %= N_COUNTER;
    pthread_mutex_unlock(&critical_section); // producer 스레드 깨우기
    
    sem_post(&semWrite);
    return n;
}

int main(){
	pthread_t t[2]; //스레드 구조체
    
    srand(time(NULL)); //난수 발생을 위한 seed 생성
    
    pthread_mutex_init(&critical_section, NULL); //뮤텍스 락 초기화
    
    //세마포 초기화 : N_COUNTER 개의 자원으로 초기화
    sem_init(&semWrite, 0, N_COUNTER); //가용버퍼의 개수를 N_COUNTER로 초기화
    sem_init(&semRead, 0, 0); //가용버퍼의 개수를 0으로 초기화
    
    //producer와 consumer 스레드 생성
    pthread_create(&t[0], NULL, producer, NULL); //생산자 스레드 생성
    pthread_create(&t[1], NULL, consumer, NULL); //소비자 스레드 생성 
    
    for(int i = 0; i<2; i++)
    	pthread_join(t[i], NULL); //모든 스레드가 소멸할 때까지 대기
        
    sem_destroy(&semRead); //세마포 기능 소멸
    sem_destroy(&semWrite); //세마포 기능 소멸
    pthread_mutex_destroy(&critical_section); //뮤텍스 락 소멸
    return 0;
}


이 글이 문제가 된다면 삭제하겠습니다.

profile
데이터사이언스와 자연어처리를 공부하고 있습니다.

0개의 댓글