동기화
여러 프로세스/쓰레드를 동시에 실행해도 공유 데이터의 일관성을 유지하는 것
임계 구역(critical section)
공유 데이터의 일관성을 보장하기 위해 하나의 프로세스/스레드만 진입해서 실행 가능한 영역
레이스 컨디션(race condition)
- 임계 구역에 동시에 접근하면 자원의 일관성이 깨질 수 있다.
- 이를 레이스 컨디션이라고 한다.
- (여러 프로세스/쓰레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황)
임계구역 문제의 해결책이 되기 위한 조건
- 상호배제(mutual exclusion): 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 들어올 수 없다.
- 진행(progress): 임계 구역에 어떤 프로세스도 진입하지 않았다면 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
- 한정된 대기(bounded waiting): 한 프로세스가 임계 구역에 진입하고 싶다면 언젠가는 임계 구역에 들어올 수 있어야 한다.
스핀락
스핀락을 사용하려는 쓰레드는 락이 해제될 때까지 반복적으로 확인(바쁜 대기, Busy-waiting)한다.
운영체제가 개입하지 않으며 쓰레드는 계속 실행 상태를 유지함.
구성 요소
- 자물쇠 역할: 프로세스들이 공유하는 전역 변수 lock
- 임계 구역을 잠그는 역할: acquire 함수
- 임계 구역의 잠금을 해제하는 역할: release 함수
특징
- 락이 점유 중인 동안 CPU를 소모하며 계속 루프를 돌며 확인.
- 블로킹 상태로 전환하지 않아 컨텍스트 스위칭 비용 없음.
바쁜 대기
- 락이 걸려있는지 계속 반복하면서 락을 얻기위해 시도하는 방식을 스핀락이라고 한다.
- (락을 가질 수 있을 때까지 반복해서 시도)
- 반복적으로 무한히 대기하면서 임계구역이 열려 있는지 아닌지 확인해보는 방식을 바쁜 대기라 한다.
- 하지만 이 방식은 락을 기다리는 동안 CPU 계속 낭비한다는 단점이 있다.
뮤텍스
뮤텍스를 획득하려는 쓰레드는 잠겨 있다면 대기 상태로 전환된다.
운영체제가 스케줄링을 통해 다른 작업을 실행하며 락이 해제되면 다시 활성화
즉, 락을 가질 수 있을 때까지 휴식을 취하는 방식
특징
- 락을 점유한 스레드가 락을 해제할 때까지 대기.
- 운영체제가 개입하여 대기 중인 스레드는 블로킹 상태로 전환.
- 잠들고 깨우는 과정에 컨텍스트 스위칭 비용이 발생.
그렇다면 뮤텍스 방식이 스핀락 방식보다 항상 좋은걸까?
- 그것은 아니다.
- 멀티 코어 환경이고, 임계 구역에서의 작업이 컨텍스트 스위칭보다 더 빨리 끝난다면 스핀락이 뮤텍스보다 더 이점이 있다.
뮤텍스와 스핀락의 주요 차이점
특징 | 뮤텍스(Mutex) | 스핀락(Spinlock) |
대기 방식 | 운영체제에 의해 스레드가 블로킹됨 | CPU를 사용하여 바쁜 대기 진행 |
CPU 사용 | 대기 중인 스레드는 CPU를 사용하지 않음 | 대기 중인 스레드가 CPU를 계속 사용 |
컨텍스트 스위칭 | 필요 시 컨텍스트 스위칭 발생 | 컨텍스트 스위칭 없음 (멀티 코어 환경에서) |
적합한 상황 | 락 점유 시간이 길거나 CPU가 바쁠 때 | 락 점유 시간이 매우 짧을 때 |
세마포
세마포(Semaphore)는 멀티스레드 환경에서 공유 자원에 대한 접근을 제어하기 위해 사용하는 동기화 도구이다.
구성 요소
- 임계 구역에 진입할 수 있는 프로세스의 개수(사용 가능한 공유 자원의 개수)를 나타내는 전역 변수 S
- 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려주는 wait 함수
- 임계 구역 앞에서 기다리는 프로세스에 '이제 가도 좋다' 고 신호를 주는 signal 함수
특징
- 하나 이상의 프로세스/쓰레드가 임계 구역에 접근 가능할 수 있다.
- 세마포는 카운터를 기반으로 작동하며, 일정 개수의 쓰레드만 공유 자원에 동시에 접근할 수 있도록 제어한다.
- 일반적으로 세마포의 카운터 값은 허용 가능한 접근 가능 자원의 수를 나타낸다.
세마포는 상호 배제를 위한 동기화 뿐만 아니라 실행 순서 보장도 가능하다.
- 세마포의 변수 S를 0으로 두고,
- 먼저 실행할 프로세스 뒤에 signal 함수
- 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 된다.
종류
- 이진 세마포(Binary Semaphore)
- 카운터 값이 0 또는 1인 세마포.
- 뮤텍스와 동일하게 동작하며, 하나의 스레드만 자원에 접근 가능.
- 하지만 뮤텍스는 락을 가진 자만 락을 해제 할 수 있지만 세마포는 그렇지 않다.
- 계수 세마포(Counting Semaphore)
- 카운터 값이 2 이상일 수 있는 세마포.
- 여러 스레드가 자원에 접근할 수 있도록 허용.
뮤텍스와 세마포의 차이점
특징 | 뮤텍스(Mutex) | 세마포(Semaphore) |
카운터 | 없음 (0 또는 1 상태만 가짐) | 카운터 값 존재 (0 이상) |
자원 허용량 | 1개 스레드만 접근 가능 | N개 스레드 접근 가능 |
소유권 | 락을 소유한 스레드만 해제 가능 | 누구나 해제 가능 |
사용 목적 | 단일 스레드 접근 제어 | 다중 스레드 접근 제어 |
- 상호 배제만 필요하다면 뮤텍스를, 작업 간의 실행 순서 동기화가 필요하다면 세마포를 권장한다.
모니터
"매번 임계 구역 앞뒤로 wait, signal 함수를 호출해야 할까?"
"그러다 실수를 하면 어떻하지?" 그래서 등장한 모니터
- 사용자(개발자)가 다루기에 편한 동기화 도구
- 상호 배제를 위한 동기화 뿐만 아니라 실행 순서 제어를 위한 동기화까지도 제공
- 조건에 따라 쓰레드가 대기 상태로 전환 가능
- 모니터 안에는 하나의 프로세스만이 있을 수 있다
사용 상황
- 한번에 하나의 쓰레드만 실행되어야 할 때
- 여러 쓰레드와 협업이 필요할 때
특징
- 상호 배제(Mutual Exclusion)
- 특정 공유 자원에 대해 하나의 스레드만 접근할 수 있도록 보장.
- 모니터는 내부적으로 락(Lock)을 사용하여 동작.
- 조건 변수(Condition Variables)
- 프로세스나 쓰레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수
- wating queue를 가짐(조건이 충족되길 기다리는 쓰레드들이 대기 상태로 머무는 곳)
- 특정 조건이 충족될 때까지 스레드가 대기하거나 깨어날 수 있도록 지원.
- wait(), notify(), notifyAll() 메서드로 쓰레드 간 협력 가능
- 자원 보호
- 모니터는 자원을 감싸는 임계 영역(Critical Section)을 관리하여 쓰레드 간 충돌을 방지.
Condition Variables에서 가지는 주요 동작
1. wait
- 쓰레드가 자기 자신을 조건 변수의 wating queue에 넣고 대기 상태로 전환
2. signal / notify
- wating queue에서 대기중인 쓰레드 중 하나를 깨움
3. broadcast / notifyAll
- wating queue에서 대기중인 쓰레드 전부를 깨움
자바 모니터
자바에서는 모든 객체가 객체의 모니터 역할을 하는 고유 락을 가지고 있다.
고유 락을 사용해서 쓰레드 간의 동기화를 수행할 수 있다.
객체 필드에 배타적인 접근이 필요한 경우 쓰레드가 객체의 고유 락을 얻어야만 접근 가능하다.
모니터의 상호 배제 기능은 synchronized 키워드로 사용한다.
자바의 모니터는 조건 변수(condition variable)를 하나만 가진다.
자바 모니터의 세 가지 동작
- wait
- notify
- notifyAll
고유 락 획득시
- 고유 락을 얻지 못한 쓰레드는 접근 불가
- BLOCKED 상태로 전환
notify( )
- 임계 구역에서 할 일을 모두 마치면 notify( ) 함수를 호출하게 됨
- wait( )으로 대기 중인 쓰레드만 깨움
- wiat( )으로 대기 중인 쓰레드가 없는 경우 무시됨
- JVM 이 BLOCKED 상태의 쓰레드를 임의로 선택함
wait( )
- 조건을 충족하지 않았으면 대기(wait)하고 BLOCKED 상태의 임의의 쓰레드가 고유 락 획득
synchronized
- 특정 메서드나 코드 블록에 대한 동시 접근 제한
- 여러 쓰레드 사이의 동기화 지원
- notify( ), wait( ), notifyAll( ) 메서드는 반드시 synchronized 내에서 호출해야 함
synchronized 사용하기
- 메서드 수준 동기화: 여러 쓰레드가 메서드 호출 불가
- 블록 수준 동기화: 코드 블록 내부만 동기화
JAVA 고유 락의 특징
- 재진입성: 쓰레드는 이미 가지고 있는 고유 락을 다시 얻지 않아도 된다.
- 가시성: 어떤 쓰레드가 변경한 값은 다른 쓰레드에서도 볼 수 있다.
재진입성
public synchronized void sayFirst() {
System.out.println("FIRST METHOD");
saySecond(); // sayFirtst 내부에서 예외 발생 없이 saySecond 메서드 호출 가능
}
public synchronized void saySecond() {
System.out.println("SECOND METHOD");
saySecond();
}
public static void main(String[] args) {
final Example example = new Example();
example.sayFirst();
}
- 고유 락은 쓰레드 단위로 획득 가능
- 이미 락을 가진 쓰레드는 대기할 필요가 없다.
가시성
happens-before 관계
a를 "새로운 값"으로 변경했을 때 쓰레드 로컬 메모리에는 a="새로운 값"이 저장되어 있지만 메인 메모리에서는 a="기존값"으로 남아 있다.
쓰레드가 synchronized 블록을 떠날 때
쓰레드 로컬 메모리에 있는 값을 메인 메모리로 flush하는 과정이 일어나므로 따라서 메인 메모리가 쓰레드 로컬 메모리에 있는 값으로 동기화가 된다.
새로운 쓰레드가 진입할 때
메인 메모리에 있는 "새로운 값"이 반영되어 있을 때 쓰레드가 synchronized 블록 내부로 진입하는 경우에 쓰레드가 메인 메모리에서 쓰레드 로컬 메모리로 refresh 해오는 과정을 수행해서 가시성을 보장하게 함
이외에도 java.util.concurrent 에는 동기화 기능이 탑재된 여러 클래스들이 있다.
'CS' 카테고리의 다른 글
[CS] 인터럽트와 시스템 콜 (1) | 2024.12.03 |
---|---|
[CS] CPU bound, IO bound (1) | 2024.12.03 |
[CS] 병행성과 병렬성 (0) | 2024.11.06 |
[CS] PCB와 TCB (0) | 2024.11.06 |
[CS] 파일 시스템 (0) | 2024.05.27 |