
DeadLock이란 여러 프로세스(또는 스레드)가 서로가 가진 자원을 기다리며 영원히 진행하지 못하는 상태를 말한다.
Deadlock이 발생하려면 아래 네 가지 조건이 동시에 모두 충족되어야 한다.
1. 상호 배제(Mutual Exclusion)
2. 점유와 대기(Hold and Wait)
3. 비선점(No Preemption)
4. 순환 대기 (Circular Wait)
교착 상태 발생 조건 중 하나를 제거함으로써 데드락을 해결하는 방법이다.
1. 상호 배제(Mutual Exclusion)
2. 점유와 대기(Hold and Wait)
3. 비선점(No Preemption)
4. 순환 대기 (Circular Wait)
Wait Free와 Lock Free는 둘 다 병렬 프로그래밍에서의 동기화 매커니즘으로 사용되며, 둘 다 락을 사용하지 않고 여러 스레드가 안전하게 공유 자원에 접근하는 방법을 말한다.
전체 시스템은 멈추지 않지만, 어떤 스레드는 밀릴 수 있음.
스레드 A: 빠르게 진행
스레드 B: A보다 느려서 자꾸 충돌 -> 계속 재시도
=> A는 계속 작업을 하고 있지만, B는 뒤에서 계속 대기
=> 전체는 잘 돌아가지만, B는 작업을 일부 하다가 실패하고 다시 처음부터 재시도하는 상황이 계속 반복됩니다.
모든 스레드가 제한 시간 안에 완료되는 구조.
스레드 A: 1초 이내에 항상 작업 끝냄
스레드 B: 1초 이내에 항상 작업 끝냄
...모든 스레드 동일
=> 어떤 스레드든 "기다림 없이" 자신의 작업을 끝냄
---
스레드 A 작업 완료 (0.1초)
스레드 B 작업 진행 중이었는데 시간 끝 → 다음 차례
스레드 A 다시 작업 → 완료
스레드 B 지난번 멈춘 지점부터 이어서 → 완료
=> 각 스레드의 진행이 보장된다.
1. 소스 코드 작성 : 개발자가 프로그래밍 언어로 소스 코드를 작성한다. (.c, .java)
2. 컴파일러 : 소스 코드를 기계어로 번역하는 단계로, 컴파일러는 소스 코드를 분석하고 최적화하여 중간 코드나 직접 실행 가능한 기계 코드를 작성하여 오브젝트 파일(.o, .class)을 생성한다.
3. 링커 : 컴파일된 파일들(여러 오브젝트 파일들)을 하나로 묶어 실행 가능한 파일(a.out, .exe)을 생성한다. 이 과정에서 외부 라이브러리나 다른 모듈과의 연결이 이루어진다.
4. 로더 : 링크된 실행 파일을 메모리에 적재하고, 프로세스로 실행을 시작한다. 로더는 필요한 라이브러리와 함께 프로세스의 주소 공간에 적재한다.
5. 실행 : CPU가 해당 명령어를 실행한다.
링커는 컴파일된 재배치 가능 오브젝트 파일을 하나의 이진 실행 파일로 결합한다. 재배치 가능 오브젝트 파일이란, 임의의 물리 메모리 위치에 적재되도록 설계된 오브젝트 파일로 컴파일되는 소스파일을 의미한다.
로더는 이진 실행 파일을 메모리에 적재한다.
자바의 경우, 컴파일의 결과로 .class 파일이 생성된다. .class 파일은 각 클래스 단위로 독립적으로 존재하는데, 실행 시 JVM이 필요한 클래스를 동적으로 로딩하고 심볼을 연결한다. 따라서 링커가 별도로 필요하지 않다.
또한, JVM이 실행되면 클래스로더가 .class 파일을 찾아서 메모리에 동적으로 로드하기 때문에 운영체제의 정적 로더에 의존하지 않는다.
| 항목 | 링커 (Linker) | 로더 (Loader) |
|---|---|---|
| 시점 | 컴파일 직후 | 실행 직전 |
| 역할 | 여러 오브젝트 파일 + 라이브러리 → 하나의 실행파일 생성 | 실행파일을 메모리에 적재하여 실행 |
| 예시 | main.o + math.o → a.out | a.out → 메모리 로딩 → 프로세스 시작 |
JIT 컴파일러란, Just In Time의 약자로, **프로그램 실행 중에 바이트코드를 기계어로 변환하는 컴파일 방식을 의미한다.
javac)이 자바 소스코드를 바이트코드로 컴파일한다. (.java -> .class)
fork(): 프로세스를 복제
exec(): 지정한 실행 파일을 메모리에 로드하여 실행
exec() 시스템 콜 내부에서 OS의 로더가 실행파일을 메모리에 올리는 역할을 수행한다. 프로그램의 실행 파일을 분석하고, 메모리 배치를 수행하는 실질적인 작업을 담당한다.IPC (Inter-Process Communication, 프로세스 간 통신)이란, 서로 독립된 프로세스 간에 데이터를 주고받는 방식이다. 운영체제는 메모리 보호 때문에 각 프로세스의 메모리를 격리시켜두기 때문에 프로세스끼리는 직접 서로의 메모리에 접근할 수 없고, 특수한 메커니즘을 통해서만 통신할 수 있다.
부모-자식 간 통신에 주로 사용된다. 단방향이며 익명 파이프는 동일한 프로세스 계열에서만 사용이 가능하다.
파이프를 파일 시스템에 등록한 형태로, 부모 프로세스와 무관하게 서로 전혀 다른 프로세스도 통신할 수 있다.
운영체제 커널에 메시지를 저장하고 전달한다. 큐 형태로 동기화가 가능하다.
가장 빠른 통신 방법으로, 공유 메모리가 데이터 자체를 공유하도록 지원한다. 즉, 여러 프로세스가 하나의 메모리 공간(공유 메모리 세그먼트)에 직접 접근할 수 있도록 하는 방식이다.
네트워크 통신 기반 방식으로, 로컬 프로세스 간에도 사용이 가능하다.
프로세스 간 간단한 이벤트/상태 알림을 위한 신호를 전송할 수 있는 방식이다.
운영체제 메시지 큐는 단방향 또는 양방향 둘 다 가능하다.
기본 설계는 단방향으로, 한 쪽 프로세스가 메시지를 보내고, 다른 쪽 프로세스가 그 메시지를 받는 구조이다.
1. 단방향 메시지 큐 : 프로세스나 스레드가 메시지를 큐에 보낼 수 있지만, 직접적인 응답을 기대하지 않는다. 다른 프로세스나 스레드는 전달된 메시지를 읽고 처리할 수 있다.
2. 양방향 메시지 큐 : 프로세스나 스레드는 요청 메시지를 보내고 응답을 기대한다. 수신 프로세스는 요청을 처리하고 응답 메시지를 발신자에게 다시 보낼 수 있다. 요청-응답 패턴으로도 불린다.
정리하자면, 단방향 메시지 큐는 프로세스가 다른 프로세스에게 알림이나 정보를 전달해야 할 경우에 적합하며, 양방향 메시지 큐는 요청이 보내지고 응답이 수신되어야 하는 더 상호작용적인 통신이 필요한 경우에 유용하다.
여러 스레드가 동시에 하나의 자원에 접근해도 문제가 생기지 않도록 보호된 상태이다.
여러 스레드가 같은 변수를 동시에 증가시키면, Race Condition이 발생하고, 결과를 예측할 수 없게 된다.
Thread safe하다는 건 이런 상황에서도 결과가 항상 일관성있게 나오고, 내부 상태가 꼬이지 않도록 보장된다는 의미이다.
1. 상호 배제(Mutual Exclusion)
2. 불변 객체 (Immutable Objects)
3. 스레드 로컬 변수 (Thread-Local Variables)
4. 원자적 연산 (Atomic Operation)
AtomicInteger5. 동기화 컬렉션 (Synchronized Collection)
Collections.synchronizedList, Collections.synchronizedMap 등의 메서드를 사용하여 동기화된 컬렉션을 생성할 수 있다.꼭 락이 필요한 것은 아니다.
락 없이 Thread Safe를 구현하는 대표적인 방법 :
| 방법 | 설명 |
|---|---|
| Atomic 변수 | 예: Java의 AtomicInteger, C++의 std::atomic |
| 불변 객체 | 객체의 상태를 바꿀 수 없게 만듦 (Java의 String, Python의 tuple) |
| Thread-local | 변수 스레드마다 데이터 별도 관리해서 공유 자체를 막음 |
| Lock-free, Wait-free 알고리즘 | CAS(Compare And Swap) 등의 기법으로 공유자원 동기화 없이 구현 |
락은 직관적이고 강력하지만, 오버헤드와 데드락 위험이 있기 때문에 상황에 따라 다양한 방법을 이용할 수 있다.
두 개 이상의 프로세스의 동기화 문제를 해결하는 방법 중 하나로, 두 개의 스레드가 공유 자원에 교대로 접근할 수 있도록 보장하는 고전적인 알고리즘이다.
핵심 아이디어
int turn;
boolean flag[2];
turn 은 critical section에 들어갈 수 있는 프로세스를 나타내는 변수이고, flag 는 critical section에 들어갈 준비 상태를 나타내는 변수이다. 코드
public static void criticalSection(int i, int j) {
for (int k = 0; k < 1000000; k++) {
// 진입 의사 표현
flag[i] = true;
turn = j;
// 상대방이 진입 의사가 있고, 내 차례가 아니면 대기
while (flag[j] && turn == j) {
// busy-wait (Spin)
}
// ----- 크리티컬 섹션 -----
sharedData++;
// -------------------------
flag[i] = false; // 나왔다는 신호
}
}
turn, flag 설정 순서가 바뀌면 알고리즘이 제대로 동작하지 않는다. volatile 키워드로 문제를 해결한다. 다중 스레드 또는 프로세스 환경에서 발생하는 문제로, 두 개 이상의 스레드가 하나의 공유 자원 혹은 임계 구역에 접근하여 수정하려고 할 때, 실행 순서에 따라 결과가 달라지는 상황을 말한다.
예시
int count = 0;
Thread A: count += 1;
Thread B: count += 1;
해결방법