06. 스레드 동기화

하이솝·2026년 4월 14일

운영체제

목록 보기
6/9

강의 목표

  • 스레드 동기화의 정의와 필요 이유를 이해한다.
  • 스레드 동기화를 위한 상호배제 임계구역에 대해 이해한다.
  • 상호배제를 구현하는 여러 방법을 이해하고
    최종적으로 원자 명령에 대해 이해한다.
  • 응용프로그램에서 사용 가능한 멀티스레드 동기화 기법들을 안다.
    뮤텍스, 스핀락, 세마포
    pthread 라이브러리를 이용하여 응용프로그램에서 스레드 동기화 실습
  • 생산자 소비자 문제를 이해하고 해결 방법을 안다

01. 스레드 동기화의 필요성

다수의 스레드가 동시에 공유 데이터에 접근하면

  • 공유데이터가 훼손되는 문제 발생 가능
    사례1) 두 스레드가 동시에 공유 데이터를 읽는 경우 → 문제 없음
    사례2) 한 스레드는 쓰고, 한 스레드는 읽을 경우
    → 읽고 쓰는 순서에 따라 읽는 값은 변화할 수 있지만,
    공유데이터의 훼손은 없음

    사례3) 두 스레드가 동시에 공유 데이터에 쓰는 경우공유 데이터 훼손 가능

스레드 동기화(thread sychronization)

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

    2) 한 스레드가 공유데이터를 배타적 독점적으로 접근하도록 순서화

1.1 공유 집계판 문제


1.2 공유 데이터 액세스 문제와 해결방법

공유 데이터에 대한 멀티스레드의 동시 접근 문제의 원인 분석

  • 두 학생: 스레드 T1, T2
  • 공유 집계판: 공유 변수 sum
  • 계산식: sum = sum + 10;

공유 데이터에 대한 동시 접근 문제의 해결책

문제점

  • 여러 스레드가 공유 변수에 접근할 때, 공유 데이터 훼손

해결책: 스레드 동기화

  • 한 스레드가 공유 데이터 사용을 마칠 때까지
    다른 스레드가 공유 데이터에 접근하지 못하도록 제어

멀티스레드의 경쟁 상황 발생 빈도

  • 매우 자주 발생
    사용자의 멀티스레드 프로그램에서 자주 발생
    커널 코드에서 매우 자주 발생 - 커널에 공유 데이터가 많기 때문

  • 다중 코어에서 더욱 조심


1.3 임계 구역과 상호배제

임계구역(ciritical section): 공유 데이터에 접근하는 프로그램 코드들

상호배제(mutual exclusion): 임계구역을 오직 한 스레드만 배타적 독점적으로 사용하도록 하는 기술

  • 임계구역에 먼저 진입한 스레드가 임계구역의 실행을 끝낼 때 까지
    다른 스레드가 진입하지 못하도록 보장

02. 상호배제


2.1 상호배제 위치


1) 일반 코드(non-critical code)

  • 공유 데이터를 액세스하지 않는 코드

2) 임계구역 진입 코드(entry code)

  • 상호배제를 위해 필요한 코드
    임계구역에 진입하기 전 필요한 코드 블록
  • 현재 임계구역을 실행 중인 스레드가 있는지 검사
    없을 때: 다른 스레드가 들어오지 못하도록 조치
    있을 때: 진입이 가능해질 때까지 대기

3) 임계구역 코드(critical code)

  • 공유 데이터에 접근하는 코드 블록
    임계구역은 짧을수록 좋음

4) 임계구역 진출 코드(exit code)

  • 상호배제를 위해 필요한 코드
    임계구역의 실행을 마칠 때 실행되어야 하는 코드 블록
  • 진입 코드에서 대기중인 스레드가 임계구역에 진입할 수 있도록
    진입 코드에서 취한 조치를 해제하는 코드

2.2 상호배제 구현

상호배제 구현 목표

  • 임계구역에 오직 1개의 스레드만 진입

상호배제 구현 방법

  • 1) 소프트웨어적 방법
    알고리즘 수준에서 제시된 것들로, 구현 시 여러 문제 노출
    peterson's 알고리즘 등

  • 2) 하드웨어적 방법
    하드웨어의 도움을 받는 방법
    인터럽트 서비스 금지, 원자 명령 활용 등
    대부분 하드웨어적 방법을 사용함
    방법 1: 인터럽트 서비스 금지
    인터럽트 서비스를 금지하거나 허용하는 CPU 명령 사용
    방법 2: 원자 명령(atomic instruction) 사용
    원자 명령은 CPU 명령
    오늘날 상호배제 구현에 사용하는 방법


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

  • 임계구역 진입 코드(entry code)에서 인터럽트 서비스를 금지하는 명령 실행
    IF(Interrupt Flag): CPU 내부의 플래그 레지스터의 비트
    비트값 1: 인터럽트 서비스 허용
    비트값 0: 인터럽트 서비스 무시

    cli(Clear Interrupt Flag)
    sti(Set Interrupt Flag)

  • 장치로부터 인터럽트가 발생해도, CPU가 인터럽트 발생을 무시
    인터럽트가 발생하지 않는것이 아님, 발생을 무시

  • 인터럽트가 발생해도 CPU는 인터럽트 서비스 루틴을 실행하지 않음

  • 인터럽트를 무시하면 임계구역을 실행하는 스레드가 중단되지 않음

문제점

  • 모든 인터럽트가 무시되는 문제 발생
  • 멀티 코어 CPU나 다중 CPU를 가진 시스템에서 활용 불가
    한 스레드가 인터럽트 서비스를 금지시킨다고 하더라도,
    다른 코어의 인터럽트 서비스까지 금지시키지는 못함

인터럽트를 금지하지 않은 경우

인터럽트를 금지시킨 경우


2.4 방법2 - 원자명령(atomic instruction) 사용

원자명령 없이 lock 변수를 이용한 상호배제 시도

  • lock 변수값 1: 잠금 상태
  • lock 변수값 0: 열림 상태

lock 변수로 상호배제 성공 사례

lock 변수를 이용한 상호배제의 근본적인 문제

lock 변수로 상호배제 실패 사례


2.5 원자명령을 이용한 상호배제

lock 변수를 사용한 경우 상호배제가 실패한 원인

  • 실패 원인은 entry code에 있음
  • lock 변수 값을 읽는 명령과 lock 변수에 1을 저장하는 2개의 명령 사이에
    컨텍스트 스위칭이 될 때 문제 발생

해결 방법 - 원자명령 도입

  • lock 변수를 읽어들이는 명령과 lock 변수에 1을 저장하는 2개의 명령을
    한번에 처리하는 원자명령 필요
  • 원자명령(Test and Set Lock TSL)

TSL을 원자명령이라 명명한 이유?
2개의 명령어를 합쳐 1개의 명령으로 만들고 TSL 명령 실행 중간에
인터럽트 서비스나 컨텍스트 스위칭이 발생하지 못하도록 하였다는 의미

03. 멀티스레드 동기화 기법

멀티스레드 동기화란?

  • 상호배제 기반(원자 명령을 기반으로 구성된 lock) 위에 자원을 사용하려는 여러 스레드들이 자원을 원활이 공유하도록 하는 기법
  • 동기화 프리미티브(sychronization primitives)로 부름

대표적인 기법

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

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

3.1 뮤텍스

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

뮤텍스 기법의 구성요소

1) 락 변수

  • true/false 중 한 값
  • true: 락을 잠근다/소유한다.
  • false: 락을 연다/해제한다

2) 대기 큐

  • 락이 열리기를 기다리는 스레드

3) 연산

  • lock 연산(임계구역의 entry 코드)
    락이 열린 상태이면, 락을 잠그고 임계구역 진입
    락이 잠김 상태이면, 현재 스레드를 block 상태로 만들고 대기 큐에 삽입
  • unlock 연산(임계구역의 exit 코드)
    락이 잠김 상태이면, 열린 상태로 변경
    대기 큐에서 기다리는 스레드 하나 깨움

뮤텍스를 활용한 스레드 동기화 과정

뮤텍스의 특징

임계구역의 실행 시간이 짧은 경우 비효율적

  • 락이 잠겨 있으면 (컨텍스트 스위칭 후)대기 큐에서 대기,
    락이 풀리면 (컨텍스트 스위칭 후)실행
  • 락이 잠겨있는 시간보다 스레드가 잠자고 깨는 데
    걸리는 시간이 상대적으로 크면 비효율적

pthread 라이브러리의 뮤텍스

뮤텍스락 변수

  • pthread_mutex_t lock;

뮤텍스 조작 함수

  • pthread_mutex_init(): 뮤텍스락 변수 초기화
  • pthread_mutex_lock(): 뮤텍스락 잠그기
  • pthread_mutex_unlock(): 뮤텍스락 풀기
  • pthread_mutex_detroy(): 뮤텍스락 변수 사용 종료

3.2 스핀락(spinlock)

  • busy-waiting lock 기법
    스레드가 큐에서 대기하지 않고 락이 열릴 때 까지 계속 락 변수 검사

  • 공격적인 뮤텍스(agressive mutex)라고도 함

  • 뮤텍스와 거의 같고, busy-waiting이라는 점에서만 다름
    대기 큐 없음
    busy-waiting으로 인해 CPU를 계속 소모,
    CPU가 다른 스레드를 실행할 수 없음

  • 락을 소유한 스레드만 자원 배타적 사용, 동기화 기법
    공유 자원 하나 당 하나의 스핀락 사용

스핀락 기법의 구성요소

1) 락 변수

  • true/false 중 한 값
  • true: 락을 잠근다/소유한다.
  • false: 락을 연다/해제한다.

2) 연산

  • lock 연산
    임계구역에 들어갈 때 실행되는 entry 코드
    락이 잠금 상태면, 락이 풀릴 때 까지 무한 루프 돌면서 lock 연산 시도
    락이 열린 상태면, 락을 잠김 상태로 바꾸고 임계구역 실행

  • unlock 연산
    임계구역을 나올 때 실행하는 exit 코드
    락을 열림 상태로 변경

스핀락을 사용한 동기화 사례

스핀락의 특징

임계 구역의 실행 시간이 짧은 경우 효율적

  • 뮤텍스의 non-blocking 모델
    락이 잠겨있을 때 블록되지 않고, 락이 풀릴 때 까지 검사하는 코드 실행
  • 단일 CPU(단일 코어)를 가진 운영체제에서 비효율적, 멀티 코어에 적합

pthread 라이브러리의 스핀락

스핀락 변수

  • pthread_spin_t lock;

스핀락 조작 함수

  • pthread_spin_init(): 스핀락 변수 초기화
  • pthread_spin_lock(): 스핀락 잠그기
  • pthread_spin_unlock(): 스핀락 풀기
  • pthread_spin_detroy(): 스핀락 변수 사용 종료

TIP. 뮤텍스와 스핀락 비교

락이 잠기는 시간

  • 길 때: 뮤텍스
  • 짧을 때: 스핀락

CPU 유형

  • 단일 CPU: 뮤텍스
  • 멀티 CPU: 스핀락

사용되는 프로그램의 유형

  • 사용자 응용프로그램: 뮤텍스
  • 커널 코드: 스핀락
    커널 코드나 인터럽트 서비스 루틴은 빨리 실행되어야 하며,
    인터럽트 서비스 루틴 내에서 잠잘 수 없기 때문

스핀락을 사용하면 기아 발생 가능

  • 스핀락은 무한 경쟁 방식이기 때문에 기아 발생 가능
    락을 소유한 스레드가 락을 풀지 않고 계속 실행하거나 종료한 경우,
    코딩이 잘못된 경우

하이브리드 뮤텍스(hybrid mutex)

  • 스핀락과 뮤텍스를 혼합한 동기화 방식

  • 처음에는 스핀락처럼 대기

  • 일정 시간이 지날 때 까지 락을 획득하지 못하면
    뮤텍스처럼 작동하여 스레드를 블록 상태로 만듦.

  • 적응적 뮤텍스(adaptive mutex) 또는 적응적 스핀락(adaptive spinlock)

스레드 동기화

  • 사용자 공간에 있는 공유 자우너과 임계구역 코드

  • 커널 공간에 있는 커널 자원과 임계구역 코드

각각 배타적으로 스레드가 접근하도록 동기화되어야 함


3.3 세마포(Semaphore)

세마포 개념

  • 멀티스레드 사이의 자원 관리 기법
    n개의 공유 자원을 다수 스레드가 공유하여 사용하도록 돕는 자원 관리 기법

세마포의 구성 요소


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

  • 사용 가능한 자원의 개수를 나타내는 정수형 전역 변수
  • n으로 초기화됨(counter = n)
  • 음수: 현재 대기중인 스레드의 개수
    양수: 현재 사용 가능한 자원의 개수

4) P/V 연산

  • P연산(wait 연산): 자원 요청 시 실행하는 연산
    자원 사용 허가를 얻는 과정

  • V연산(signal 연산): 자원 반환 시 실행하는 연산
    자원 사용이 끝났음을 알리는 과정

P 연산과 V 연산

  • P/V를 wait/signal로 표기하기도 함

  • P 연산: 자원 사용을 허가하는 과정, 사용 가능 자원 수 1 감소(couner--)

  • V 연산: 자원 사용을 마치는 과정, 사용 가능 자원 수 1 증가(counter++)

  • 세마포의 종류 2가지: 자원을 할당받지 못했을 때 행동을 기준으로 구분
    1) sleep-wait 세마포
    P 연산: 대기 큐에서 잠자기, counter--;
    V 연산: 사용 가능 자원이 있으면 잠자는 스레드 깨우기, counter++;

    2) busy-wait 세마포
    P 연산: 사용 가능 자원이 생길 때 까지 무한 루프, counter--;
    V 연산: counter++;
    무한 루프를 하며 계속 대기하기 때문에 counter ≤ 0

pthread 라이브러리의 세마포

세마포 구조체

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

세마포 조작 함수

  • sem_init(): 세마포 초기화

  • sem_detroy(): 세마포 기능 소멸

  • sem_wait()
    P 연산을 수행하는 함수(blocking call)
    sleep-wait 방식으로, 가용 자원이 없으면 대기 큐에서 잠을 잠

  • sem_trywait()
    P 연산을 수행하는 함수(non-blocking call)
    가용 자원이 있으면, counter 값을 감소시키고 0 리턴
    가용 자원이 없다면, counter 값을 유지하고 -1 리턴

  • sem_post()
    V 연산을 수행하는 함수

  • sem_getValue()
    세마포의 현재 counter 값을 리턴하는 함수

busy-waiting, busy-looping, spinning

  • 모두 같은 의미를 가지고 있음
  • 스레드가 어떤 조건이 참(true)가 될 때 까지 반복 조사하면서 기다리는 기법
  • CPU를 심하게 낭비함

3.4 이진 세마포

카운터 세마포(counter semaphore)

  • 자원의 인스턴스가 여러 개인 경우
    앞에서 설명한 세마포가 그 예

이진 세마포(binary semaphore)

  • 자원이 1개 있는 경우 멀티스레드사이의 자원 관리
  • 1개의 자원에 대해 1개의 스레드만 액세스할 수 있도록 보호
    뮤텍스와 유사

이진 세마포 구성요소

1) 세마포 변수 S
0과 1 중 하나를 가지는 전역 변수 S는 1로 초기화

2) 대기 큐

  • 사용 가능한 자원이 생길 때 까지 스레드들이 대기하는 큐
  • 스레드 스케줄링 알고리즘 필요

3) 2개의 원자 연산
wait 연산(P 연산) - 자원 허가를 얻는 과정
S를 1 감소 시키고, 0보다 작으면 대기 큐에서 잠듦.
0보다 크거나 같으면 자원 사용하는 코드 실행

signal 연산(V 연산) - 자원 사용이 끝났음을 알리는 과정
S를 1 증가 시키고, 0보다 크면 그냥 리턴.
0보다 작거나 같으면 대기 큐에 있는 스레드 중 하나를 깨움

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

우선순위 역전(priority inversion)

  • 스레드의 동기화로 인해
    높은 순위의 스레드가 낮은 스레드보다 늦게 스케줄링되는 현상
    우선순위를 기반으로 스케줄하는 실시간 시스템에서
    스레드 동기화로 인해 문제 발생


T2의 실행 시간이 길어지게 되면 T1이 V를 호출하는 시점도 늦춰지므로,
더욱 심각한 문제가 발생하게 됨

실시간 시스템의 근본이 붕괴됨

우선순위 역전의 해결책

우선순위 올림(priority celling)

  • 스레드가 공유 자원을 소유하게 될 때,
    스레드의 우선순위를 미리 정해진 높은 우선순위로 일시적으로 올림

  • 선점되지 않고 빨리 실행되도록 유도

  • 우선순위는 그 어떤 스레드보다 높게 설정됨

우선순위 상속(priority inheritance)

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

우선순위를 올리는 시점의 차이가 있으며, 공유 자원 할당이 끝나게 되면 우선순위는 원래대로 돌아옴

04. 생산자 소비자 문제


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


4.2 생산자 소비자 문제의 정의

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

  • 문제1) 상호배제
    생산자들과 소비자들의 공유버퍼 사용에 대한 상호배제

  • 문제2) 비어 있는 공유버퍼 문제
    비어 있는 공유버퍼를 소비자가 읽을 때

  • 문제3) 꽉 찬 공유버퍼 문제
    꽉 찬 공유버퍼에 생산자가 쓸 때

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

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

꽉 찬 공유버퍼 문제 해결

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

생산자와 소비자 알고리즘

  • R: 버퍼에 읽기 가능한 버퍼의 개수. 0이면 대기
  • W: 버퍼에 쓰기 가능한 버퍼의 개수. 0이면 대기
  • M: 뮤텍스. 생산자와 소비자 모두 사용

0개의 댓글