본문 바로가기

CS/운영체제

비행기 화장실로 Mutex와 Semaphore를 아라보좌

728x90

프로세스 동기화는 멀티스레딩 환경에서 매우 중요한 개념인데요, iOS에서는 GCD라는 편리한 멀티스레딩 툴이 있지만, Mutex와 Semaphore라는 메커니즘도 있습니다. 이번 포스팅에서는 두 개념을 화장실에 빗대 이해해보려고 합니다.

프로세스 동기화란?

프로세스 동기화는 여러 프로세스나 스레드가 동시에 실행될 때 발생할 수 있는 문제를 해결하기 위한 방법입니다.
쉽게 말해, 여러 사람이 동시에 한 가지 일을 할 때 서로 방해되지 않게 조율하는 것과 같아요.

컴퓨터에서 여러 프로세스나 스레드가 동시에 실행되면 다음과 같은 문제가 발생할 수 있습니다.

Race condition: 여러 프로세스가 동시에 같은 데이터를 수정하려고 할 때
실행 순서/싱크 문제: 특정 작업이 다른 작업보다 먼저 실행되어야 할 때
교착 상태(데드락): 여러 프로세스가 서로가 가진 자원을 기다리며 무한히 대기하는 상황

예를 들어, 은행 계좌에서 돈을 인출하는 상황을 생각해봅시다. 만약 두 사람이 동시에 같은 계좌에서 돈을 인출하려고 한다면, 잔액 확인과 인출 과정이 꼬여서 잘못된 결과가 나올 수 있습니다. 프로세스 동기화는 이런 상황에서 한 번에 한 사람만 계좌에 접근할 수 있게 해서 문제를 방지합니다.

Mutex란?

Mutex는 "1인용 화장실"과 같습니다. 한 번에 한 사람만 사용할 수 있죠. Mutex를 사용하면 한 프로세스나 스레드만 공유 자원에 접근할 수 있습니다.

예시 시나리오:
카페에 화장실이 한 칸 뿐인 상황을 생각해봅시다. 영희, 철수, 민수 세 친구가 이 카페에 왔습니다.

장점:

  1. 단순성: 영희가 화장실에 가려고 카운터에서 키를 받아갑니다. 모두가 이 규칙을 쉽게 이해합니다.
  2. 소유권 명확: 철수와 민수는 영희가 키를 가졌다는 걸 알고 기다립니다.
  3. 문제 추적 용이: 화장실에 문제가 생기면 누가 마지막으로 사용했는지 쉽게 알 수 있습니다.

단점:

  1. 융통성 부족: 민수가 급한데도 영희가 화장실에 오래 있으면 기다려야 합니다.
  2. 대기 시간: 세 명 모두 차례로 기다려야 해서 전체 식사 시간이 길어집니다.

코드 예시:
```swift

class SingleStallBathroom {
private let mutex = NSLock()

func use(by person: String) {  
    print("\\(person)이(가) 화장실을 사용하려고 합니다.")  
    mutex.lock()  
    print("\\(person)이(가) 화장실에 들어갔습니다.")  

    // 화장실 사용 시뮬레이션  
    Thread.sleep(forTimeInterval: Double.random(in: 1...3))  

    print("\\(person)이(가) 화장실에서 나왔습니다.")  
    mutex.unlock()  
	}
}

 

Semaphore란?

Semaphore는 "비행기 화장실"과 비슷합니다. 여러 칸이 있고, 입구에 빈 칸 수를 보여주는 전광판이 있습니다. 세마포어는 동시에 사용 가능한 자원의 수를 제어합니다.

예시 시나리오:
비행기에 3개의 화장실 칸이 있고, 영희, 철수, 민수가 이 비행기에 탑승했습니다.

장점:

  1. 유연성: 영희, 철수, 민수가 동시에 화장실을 사용할 수 있어 빠르게 자리로 돌아갑니다.
  2. 자원 활용도: 승객이 많아도 3명까지는 기다리지 않고 화장실을 이용할 수 있습니다.
  3. 신호 메커니즘: 다른 승객들도 전광판을 보고 기다려야 할지 바로 들어갈 수 있을지 알 수 있습니다.

단점:

  1. 복잡성: 물리적으로 화장실의 개수가 뮤텍스보다 많아 설계와 관리가 더 복잡합니다.
  2. 소유권 불명확: 특정 칸을 누가 사용 중인지 정확히 알기 어려울 수 있습니다.
  3. 실수 가능성: 바쁜 시간에 전광판 관리를 실수하여 실제로는 빈 칸이 있는데도 모든 칸이 차 있다고 표시될 수 있습니다.

코드 예시:

class MultiStallBathroom {  
    private let semaphore: DispatchSemaphore  
    private let stallCount: Int  

    init(stallCount: Int) {  
        self.stallCount = stallCount  
        self.semaphore = DispatchSemaphore(value: stallCount)  
    }  

    func use(by person: String) {  
        print("\\(person)이(가) 화장실을 사용하려고 합니다.")  
        semaphore.wait()  
        print("\\(person)이(가) 화장실에 들어갔습니다.")  

        // 화장실 사용 시뮬레이션  
        Thread.sleep(forTimeInterval: Double.random(in: 1...3))  

        print("\\(person)이(가) 화장실에서 나왔습니다.")  
        semaphore.signal()  
    }  
}  

Mutex vs Semaphore

Mutex와 Semaphore의 주요 차이점은 다음과 같습니다:

  1. 접근 가능한 프로세스/스레드 수: Mutex는 한 번에 하나만, Semaphore는 여러 개 가능
  2. 소유권: Mutex는 명확한 소유권이 있지만, Semaphore는 상대적으로 불명확
  3. 구현 복잡성: Mutex는 단순하지만 Semaphore는 더 복잡
  4. 유연성: Mutex는 제한적이지만 Semaphore는 더 유연함
  5. 자원 활용도: Semaphore가 일반적으로 더 효율적

결론적으로, 상황에 따라 적절한 동기화 메커니즘을 선택하는 것이 중요합니다. 단순한 상호 배제가 필요하다면 Mutex를, 여러 자원을 관리해야 한다면 Semaphore를 사용하는 것이 좋습니다. 1인용 화장실(Mutex)은 관리가 쉽고 명확하지만 대기 시간이 길어질 수 있고, 비행기 화장실(Semaphore)은 효율적으로 많은 사용자를 처리할 수 있지만 관리가 더 복잡할 수 있습니다. 이러한 개념을 잘 이해하고 적용하면, 효율적이고 안전한 멀티스레딩 프로그램을 작성할 수 있습니다.

import Foundation  

// 화장실을 나타내는 클래스  
class Bathroom {  
    private let lock: NSLocking // 동기화를 위한 잠금 객체 (뮤텍스 또는 세마포어)  
    private let name: String    // 화장실의 이름  

    init(name: String, lock: NSLocking) {  
        self.name = name  
        self.lock = lock  
    }  

    // 화장실 사용 함수  
    func use(by person: String) {  
        let startTime = DispatchTime.now() // 대기 시작 시간 기록  
        print("\\(person)이(가) \\(name)을(를) 사용하려고 합니다.")  

        lock.lock() // 화장실 입장 (잠금 획득)  

        let endTime = DispatchTime.now() // 대기 종료 시간 기록  
        let waitTime = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1\_000\_000\_000  
        print("\\(person)이(가) \\(name)에 들어갔습니다. 대기 시간: \\(String(format: "%.2f", waitTime))초")  

        // 화장실 사용 (1~3초 동안 랜덤하게 사용)  
        Thread.sleep(forTimeInterval: Double.random(in: 1...3))  

        print("\\(person)이(가) \\(name)에서 나왔습니다.")  
        lock.unlock() // 화장실 퇴장 (잠금 해제)  
    }  
}  

// 1인용 화장실 (뮤텍스 사용)  
let singleBathroom = Bathroom(name: "1인용 화장실", lock: NSLock())  

// 공용 화장실 (세마포어 사용, 3개의 칸)  
let sharedBathroom = Bathroom(name: "공용 화장실", lock: DispatchSemaphore(value: 3))  

// 화장실 사용 상황 테스트 함수  
func testBathroomUsage(with bathroom: Bathroom, userCount: Int) {  
    let startTime = DispatchTime.now() // 테스트 시작 시간 기록  

    // 동시에 여러 사용자가 화장실을 사용하는 상황  
    DispatchQueue.concurrentPerform(iterations: userCount) { i in  
        bathroom.use(by: "사람\\(i+1)")  
    }  

    let endTime = DispatchTime.now() // 테스트 종료 시간 기록  
    let totalTime = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds) / 1\_000\_000\_000  
    print("\\(bathroom.name) 사용 테스트 완료. 총 소요 시간: \\(String(format: "%.2f", totalTime))초")  
}  

// 1인용 화장실 사용 테스트 (뮤텍스)  
print("1인용 화장실 사용 테스트 (뮤텍스):")  
testBathroomUsage(with: singleBathroom, userCount: 10)  

// 공용 화장실 사용 테스트 (세마포어)  
print("\\n공용 화장실 사용 테스트 (세마포어):")  
testBathroomUsage(with: sharedBathroom, userCount: 10)  

출처

[https://developer.apple.com/documentation/dispatch/dispatchsemaphore\]

[[https://medium.com/@kwoncharles/%EB%AE%A4%ED%85%8D%EC%8A%A4-mutex-%EC%99%80-%EC%84%B8%EB%A7%88%ED%8F%AC%EC%96%B4-semaphore-%EC%9D%98-%EC%B0%A8%EC%9D%B4-de6078d3c453\]

'CS > 운영체제' 카테고리의 다른 글

햄버거집 '피크타임'으로 알아보는 CPU 스케줄링  (4) 2024.07.23