로컬 변수(local variables)는 스레드 간에 공유되지 않는다
공유 가능한 것들
동일한 입력으로 실행하더라도, 스레드 실행 시점(timing)에 따라 프로그램의 결과가 매번 달라질 수 있다.
1000원의 잔고가 남아있을 때, 500원의 입금과 500원의 출금이 동시에 일어날 경우?
→ 스케줄링에 따라 잔고가 500이 될 수도, 1500이 될 수도 있다. 결과를 예측할 수 없다 (Non-deterministic).
| 조건 | 설명 |
|---|---|
| Mutual Exclusion (상호 배제) | Process A가 Critical Section에 진입해 있다면, 다른 모든 Process는 진입할 수 없어야 함 |
| Progress (진행) | 어떤 Process도 Critical Section 내에 없고, 진입하려는 Process가 존재한다면 누가 진입할지 결정할 수 있어야 함 (Deadlock-free) |
| Bounded Waiting | Process가 Critical Section에 진입할 때까지 걸리는 시간에 Limit이 존재해야 함 (Starvation-free) |
// Shared Variables
int turn = 0;
// Process P0
do {
while (turn != 0); // 대기
// critical section
turn = 1;
// remainder section
} while (1);
// Process P1
do {
while (turn != 1); // 대기
// critical section
turn = 0;
// remainder section
} while (1);
// Shared Variables
boolean flag[2] = {false, false};
// Process P0
do {
flag[0] = true;
while (flag[1]); // 대기
// critical section
flag[0] = false;
// remainder section
} while (1);
flag는 자기꺼 true, turn은 상대꺼
// Shared Variables
int turn;
boolean flag[2] = {false, false};
// Process P0
do {
flag[0] = true;
turn = 1;
while (flag[1] && turn == 1); // 대기
// critical section
flag[0] = false;
// remainder section
} while (1);
// Process P1
do {
flag[1] = true;
turn = 0;
while (flag[0] && turn == 0); // 대기
// critical section
flag[1] = false;
// remainder section
} while (1);
핵심: flag만 쓸 때의 문제(동시 true → 데드락)를 turn 변수가 해결해주고, turn만 쓸 때의 문제(진행 불가)를 flag 변수가 해결한다.
CPU에서 지원하여 원자적(Atomically) 으로 수행되는 명령어를 이용
TestAndSet()명령어는 위와 같은 응용프로그램 언어로 동작하는게 아니라 CPU의 하드웨어로서 동작한다. 하나의 의사코드로 받아들면 된다.
boolean TestAndSet(boolean *target) {
boolean rv = *target; // 1. 읽기
*target = true; // 2. 쓰기
return rv; // 3. 반환
}
// Shared Variables
boolean lock = false;
// Process Pi
do {
while (TestAndSet(&lock)); // lock이 false면 통과, true로 변경
// critical section
lock = false;
// remainder section
} while (1);
두 개의 원자적 연산을 가지는 정수 변수
// P(S)
wait(S) {
while (S <= 0); // busy waiting
S--;
}
// V(S)
signal(S) {
S++;
}
typedef struct {
int value;
struct process *list; // 대기 중인 프로세스 큐
} semaphore;
// P(S)
wait(semaphore *S) {
S->value--;
if (S->value < 0) {
// 프로세스를 S->list에 추가
block();
}
}
// V(S)
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
// S->list에서 프로세스 하나 제거
wakeup(P);
}
}
| 종류 | 설명 |
|---|---|
| Counting Semaphore | 값의 범위가 정해져 있지 않음. 초기 값은 가능한 자원의 수 |
| Binary Semaphore | 값이 0과 1만 가능. 구현이 간단함 |
P() → Critical Section → P(): V()가 없어서 DeadlockV() → Critical Section → P(): P()가 없어서 Mutual Exclusion 보장 못함Binary Semaphore를 이용한 Counting Semaphore 구현
C: count 변수 (양수: 사용 가능한 자원 수, 음수: 대기 중인 프로세스 수)S1: Mutex용 (초기값 1)S2: delay용 (초기값 0)// Wait Operation (자원 획득)
P(S1);
C--;
if (C < 0) {
V(S1);
P(S2); // 대기
}
V(S1);
// Signal Operation (자원 반환)
P(S1);
C++;
if (C <= 0) {
V(S2); // 대기자 깨움
}
V(S1);
두 개 이상의 Process들이 끝없이 이벤트를 기다리고 있는 상황. 그 이벤트는 기다리고 있는 Process만이 발생시킬 수 있는 것.
| 세마포어 | 초기값 | 역할 |
|---|---|---|
empty | n | Buffer 내에 저장할 공간이 있음을 표시 |
full | 0 | Buffer 내에 소비할 아이템이 있음을 표시 |
mutex | 1 | Buffer에 대한 접근을 관리 |
do {
// produce an item
P(empty);
P(mutex);
// add item to buffer
V(mutex);
V(full);
} while (1);
do {
P(full);
P(mutex);
// remove item from buffer
V(mutex);
V(empty);
// consume item
} while (1);
// Shared Variables
int readcount = 0;
semaphore mutex = 1;
semaphore wrt = 1;
// Writer
P(wrt);
// writing
V(wrt);
// Reader
P(mutex);
readcount++;
if (readcount == 1)
P(wrt);
V(mutex);
// reading
P(mutex);
readcount--;
if (readcount == 0)
V(wrt);
V(mutex);
문제점: Writer의 Starvation - Reader들이 계속 진입하면 readcount가 0이 되지 않음

5명의 철학자가 한 식탁에서 함께 식사. 젓가락이 5개 뿐이고, 두 젓가락을 모두 집어야 식사 가능.
// 철학자 Process
do {
P(chopstick[i]);
P(chopstick[(i+1) % 5]);
// eat
V(chopstick[i]);
V(chopstick[(i+1) % 5]);
// think
} while (1);
문제점: 모든 철학자가 동시에 왼쪽 젓가락을 집으면 Deadlock 발생
// P,V 구현
void P(semaphore *S) {
S->value--; // 값 감소
if (S->value < 0) { // 음수면 대기
add_to_queue(S->list, current_process);
block(); // 프로세스 블록 (재우기)
}
// value >= 0이면 그냥 통과
}
void V(semaphore *S) {
S->value++; // 값 증가
if (S->value <= 0) { // 대기 중인 프로세스 있으면
process *P = remove_from_queue(S->list);
wakeup(P); // 프로세스 깨우기 🔔
}
}
// Shared Variables
enum {THINKING, HUNGRY, EATING} state[5];
semaphore mutex = 1;
semaphore self[5] = {0, 0, 0, 0, 0};
void take_chopsticks(int i) {
P(mutex);
state[i] = HUNGRY;
test(i);
V(mutex);
P(self[i]);
}
void put_chopsticks(int i) {
P(mutex);
state[i] = THINKING;
test(LEFT);
test(RIGHT);
V(mutex);
}
void test(int i) {
if (state[i] == HUNGRY &&
state[LEFT] != EATING &&
state[RIGHT] != EATING) {
state[i] = EATING;
V(self[i]);
}
}
1. 생각한다 💭 (THINKING)
↓
2. 배고파진다 😋
↓
3. "양쪽 젓가락 비었나?" 확인
↓
4-A. 비었으면 → 바로 먹기 🍝 (EATING)
4-B. 안 비었으면 → 기다리기 😴 (HUNGRY 상태로 대기)
↓
5. 먹고 나서 젓가락 내려놓기
↓
6. "혹시 옆 사람 기다리나?" 확인 후 깨워주기 🔔
↓
7. 다시 생각하기 💭
위 해결 방안들은 Starvation까지 해결해주지는 못한다. -> aging으로 Starvation 해결
iOS 앱도 멀티스레딩 환경에서 동작하기 때문에, 운영체제에서 배운 동기화 개념이 그대로 적용된다.
var balance = 1000
// Thread 1
DispatchQueue.global().async {
balance += 500 // 입금
}
// Thread 2
DispatchQueue.global().async {
balance -= 500 // 출금
}
// 결과: 1000? 1500? 500? → Non-deterministic
가장 간단한 방법. Serial Queue는 한 번에 하나의 작업만 실행한다.
let serialQueue = DispatchQueue(label: "com.app.serial")
serialQueue.async {
balance += 500
}
serialQueue.async {
balance -= 500
}
let lock = NSLock()
// Thread 1
lock.lock() // P(S)
balance += 500
lock.unlock() // V(S)
// Thread 2
lock.lock()
balance -= 500
lock.unlock()
let semaphore = DispatchSemaphore(value: 1) // 초기값 1 = Binary Semaphore
// Thread 1
semaphore.wait() // P(S) - value 감소
balance += 500
semaphore.signal() // V(S) - value 증가
// Thread 2
semaphore.wait()
balance -= 500
semaphore.signal()
리소스 개수 제한에도 활용 가능하다.
// 동시에 3개의 네트워크 요청만 허용
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
DispatchQueue.global().async {
semaphore.wait() // 3개 초과시 대기
fetchData(from: url)
semaphore.signal()
}
}
Swift의 Actor는 Monitor 개념과 유사하다. 내부 상태에 한 번에 하나의 작업만 접근할 수 있도록 보장한다.
actor BankAccount {
private var balance = 1000
func deposit(_ amount: Int) {
balance += amount
}
func withdraw(_ amount: Int) {
balance -= amount
}
}
let account = BankAccount()
Task {
await account.deposit(500) // 자동으로 동기화됨
}
Task {
await account.withdraw(500) // 자동으로 동기화됨
}
Main Thread에서 semaphore.wait()이나 lock.lock()을 호출하면 UI가 멈출 수 있다. 무거운 동기화 작업은 반드시 Background Thread에서 처리해야 한다.
// ❌ 잘못된 예 - Main Thread 블로킹
DispatchQueue.main.async {
semaphore.wait() // UI 멈춤!
}
// ✅ 올바른 예 - Background에서 처리
DispatchQueue.global().async {
semaphore.wait()
// 작업 수행
semaphore.signal()
DispatchQueue.main.async {
// UI 업데이트
}
}
| 방식 | 특징 | 사용 시점 |
|---|---|---|
| Lock/Semaphore | 공유 자원 직접 보호 | 단순한 상태 보호 |
| Actor | 자동 동기화 | Swift 5.5+, 상태 캡슐화 |
| RxSwift/Combine | 스트림 기반 순차 처리 | 비동기 이벤트 체이닝, 복잡한 데이터 흐름 |