[운영체제+iOS] 동기화 (Synchronization)

hye0n.gyu·2025년 12월 17일

운영체제+iOS

목록 보기
4/4
post-thumbnail

⭐ Synchronization

🧩 Sharing Resources

  • 로컬 변수(local variables)는 스레드 간에 공유되지 않는다

    • 로컬 변수는 스택(Stack) 에 저장된다.
    • 각 스레드는 자신만의 독립된 스택을 가지고 있다.
    • 따라서 다른 스레드의 스택에 있는 로컬 변수의 포인터를 전달하거나, 공유하거나, 저장해서는 안 된다.
  • 공유 가능한 것들

    • Global variables (전역변수): static data segment에 저장, 모든 스레드가 접근 가능
    • Dynamic objects (동적 할당된 객체): heap에 저장, 포인터를 통해 공유

🧩 Race Condition

**Concurrency는 비결정적(non-deterministic) 결과를 초래한다!**

동일한 입력으로 실행하더라도, 스레드 실행 시점(timing)에 따라 프로그램의 결과가 매번 달라질 수 있다.

  • Race Condition: 공유 데이터에 대해 여러 Process가 동시에 접근, 변경을 시도하는 상황
  • 데이터 일관성을 유지하기 위해서는 Process 간의 동기화(Process Synchronization) 가 필요함
  • 동기화(Synchronization)는 동시성(Concurrency)을 제한한다
  • 스케줄링(Scheduling)은 프로그래머가 제어할 수 없다 (OS 스케줄러가 결정)

예제: 은행의 입출금 문제

1000원의 잔고가 남아있을 때, 500원의 입금과 500원의 출금이 동시에 일어날 경우?

→ 스케줄링에 따라 잔고가 500이 될 수도, 1500이 될 수도 있다. 결과를 예측할 수 없다 (Non-deterministic).


🧩 Critical Section

  • Critical Section: 여러 Process들이 공유하는 데이터에 접근하는 Code 영역
  • 한 번에 오직 하나의 Process만이 Critical Section에 진입해야 함

Critical Section 해결의 조건들

조건설명
Mutual Exclusion (상호 배제)Process A가 Critical Section에 진입해 있다면, 다른 모든 Process는 진입할 수 없어야 함
Progress (진행)어떤 Process도 Critical Section 내에 없고, 진입하려는 Process가 존재한다면 누가 진입할지 결정할 수 있어야 함 (Deadlock-free)
Bounded WaitingProcess가 Critical Section에 진입할 때까지 걸리는 시간에 Limit이 존재해야 함 (Starvation-free)

🧩 두 Process를 위한 Algorithm

Algorithm 1: turn 변수 사용

// 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);
  • ✅ 만족: Mutual Exclusion
  • ❌ 불만족: Progress, Bounded Waiting
    • P0 → P1 → P0 → P1 → … 교대로 실행되어야만 함

Algorithm 2: flag 배열 사용

// 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);
  • ✅ 만족: Mutual Exclusion
  • ❌ 불만족: Progress, Bounded Waiting
    • 두 Process가 동시에 flag를 true로 하면 Deadlock 발생

🧩 Peterson Solution

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);
  • ✅ 만족: Mutual Exclusion, Progress, Bounded Waiting
  • 두 Process가 동시에 수행되더라도, turn 값에 의하여 결정됨

핵심: flag만 쓸 때의 문제(동시 true → 데드락)를 turn 변수가 해결해주고, turn만 쓸 때의 문제(진행 불가)를 flag 변수가 해결한다.

Peterson Solution의 한계

  1. 두 개 프로세스에서만 적용 가능: 3개 이상으로 확장 시 복잡해짐
  2. Busy waiting: 무한 루프로 대기하므로 CPU 자원 낭비

🧩 Synchronization Instruction (CPU의 하드웨어로서 동작)

CPU에서 지원하여 원자적(Atomically) 으로 수행되는 명령어를 이용

TestAndSet() 명령어는 위와 같은 응용프로그램 언어로 동작하는게 아니라 CPU의 하드웨어로서 동작한다. 하나의 의사코드로 받아들면 된다.

Test and Set 명령어

boolean TestAndSet(boolean *target) {
    boolean rv = *target;  // 1. 읽기
    *target = true;        // 2. 쓰기
    return rv;             // 3. 반환
}

Mutual Exclusion with Test-and-Set

// Shared Variables
boolean lock = false;

// Process Pi
do {
    while (TestAndSet(&lock));  // lock이 false면 통과, true로 변경
    // critical section
    lock = false;
    // remainder section
} while (1);

🧩 Semaphores (세마포어)

두 개의 원자적 연산을 가지는 정수 변수

  • P() (Wait): Critical Section 들어가기 전에 수행
  • V() (Signal): Critical Section 나와서 수행

Busy Waiting 구현

// P(S)
wait(S) {
    while (S <= 0);  // busy waiting
    S--;
}

// V(S)
signal(S) {
    S++;
}

Sleep Queue 구현

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만 가능. 구현이 간단함

세마포어의 단점

  • Deadlock 발생 가능성
  • P와 V의 연산이 분리되어 있어 잘못 사용할 경우 문제 발생
    • P() → Critical Section → P(): V()가 없어서 Deadlock
    • V() → Critical Section → P(): P()가 없어서 Mutual Exclusion 보장 못함

🧩Counting Semaphore 구현

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);

🧩 Deadlock

두 개 이상의 Process들이 끝없이 이벤트를 기다리고 있는 상황. 그 이벤트는 기다리고 있는 Process만이 발생시킬 수 있는 것.


🧩 Monitor

  • High-level 언어에서의 동기화 방법 (Java의 Thread 동기화)
  • 한 순간에 하나의 Process만 Monitor에서 활동하도록 보장
  • 공유 데이터(필드값)와 공유 데이터를 접근하는 코드(함수)를 하나의 모니터(객체)에 구성
  • P와 V 연산 없이 Procedure를 호출하는 것만으로 동기화 해결

🧩 Bounded-Buffer Problem

N개의 Item을 삽입할 수 있는 Buffer에 여러 생산자(Producer)와 여러 소비자(Consumer)가 접근

세마포어

세마포어초기값역할
emptynBuffer 내에 저장할 공간이 있음을 표시
full0Buffer 내에 소비할 아이템이 있음을 표시
mutex1Buffer에 대한 접근을 관리

생산자 Process

do {
    // produce an item
    P(empty);
    P(mutex);
    // add item to buffer
    V(mutex);
    V(full);
} while (1);

소비자 Process

do {
    P(full);
    P(mutex);
    // remove item from buffer
    V(mutex);
    V(empty);
    // consume item
} while (1);

🧩 Readers-Writers Problem

여러 Readers와 Writers가 하나의 공유 데이터를 읽거나 쓰기 위해 접근
  • Readers: 여러 Reader는 동시에 Data를 읽을 수 있음
  • Writers: Writer가 데이터를 수정하기 위해서는 Reader 혹은 다른 Writer가 작업을 하고 있지 않아야 함

Solution 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이 되지 않음


🧩 Dining-Philosophers Problem

5명의 철학자가 한 식탁에서 함께 식사. 젓가락이 5개 뿐이고, 두 젓가락을 모두 집어야 식사 가능.

Solution 1 (Deadlock 발생 가능)

// 철학자 Process
do {
    P(chopstick[i]);
    P(chopstick[(i+1) % 5]);
    // eat
    V(chopstick[i]);
    V(chopstick[(i+1) % 5]);
    // think
} while (1);

문제점: 모든 철학자가 동시에 왼쪽 젓가락을 집으면 Deadlock 발생

Solution 2 (양쪽 젓가락 한꺼번에 집기)


// 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. 다시 생각하기 💭

Deadlock 개선안

  1. 한 번에 최대 4명의 철학자만 식탁에 앉도록 한다
  2. 양쪽 젓가락이 모두 사용 가능할 때만 젓가락을 집도록 한다
  3. 홀수 철학자는 왼쪽, 짝수 철학자는 오른쪽 젓가락을 먼저 집도록 한다

위 해결 방안들은 Starvation까지 해결해주지는 못한다. -> aging으로 Starvation 해결

⭐ iOS에서의 동기화

iOS 앱도 멀티스레딩 환경에서 동작하기 때문에, 운영체제에서 배운 동기화 개념이 그대로 적용된다.

🧩 Race Condition in iOS

var balance = 1000

// Thread 1
DispatchQueue.global().async {
    balance += 500  // 입금
}

// Thread 2
DispatchQueue.global().async {
    balance -= 500  // 출금
}

// 결과: 1000? 1500? 500? → Non-deterministic

🧩 iOS의 동기화 도구들

1. DispatchQueue (Serial Queue)

가장 간단한 방법. Serial Queue는 한 번에 하나의 작업만 실행한다.

let serialQueue = DispatchQueue(label: "com.app.serial")

serialQueue.async {
    balance += 500
}

serialQueue.async {
    balance -= 500
}

2. NSLock (Binary Semaphore와 유사)

let lock = NSLock()

// Thread 1
lock.lock()      // P(S)
balance += 500
lock.unlock()    // V(S)

// Thread 2
lock.lock()
balance -= 500
lock.unlock()

3. DispatchSemaphore (Counting Semaphore)

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()
    }
}

4. Actor (Swift 5.5+, Monitor와 유사)

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 동기화 주의사항

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스트림 기반 순차 처리비동기 이벤트 체이닝, 복잡한 데이터 흐름
profile
반려묘 하루 velog

0개의 댓글