재진입 가능한 세마포어 이해하기

목차

  • 상호 배제성
  • 재진입 가능성
  • synchronized 키워드를 통한 자동 락 관리
  • 디락 예방 전략
  • 요약

동기화 메커니즘은 여러 스레드가 공유 리소스에 접근할 때 동작을 제어하는 데 사용됩니다.

병렬 처리에서 임계 구간은 특정 리소스에 대한 접근을 제한하는 코드 블록으로, 동시에 하나의 스레드만 실행할 수 있습니다.

락 과정: 리소스 접근 전에 락을 획득해야 하며, 이미 다른 스레드가 소유하고 있을 경우 대기 상태가 됩니다.

임계 구간 실행: 락을 획득한 후 리소스를 처리합니다.

해제: 리소스 사용이 완료되면 락을 해제하여 다른 스레드가 접근할 수 있도록 합니다.

락 객체 자체는 임계 구간이 아니며, 리소스 보호를 위한 도구입니다.

1. 상호 배제성

ReentrantMutex를 사용한 동시성 제어

package Study
// 라이브러리 참조
import std.sync.*
import std.collection.*
 
var count = 100
// 재진입 가능한 세마포어
let mutex = Mutex()
 
main () {
    let tasks = ArrayList()
 
    // 100개의 스레드 생성
    for (_ in 0..100) {
        let task = spawn {
            // 락 획득
            mutex.lock()
            count++
            // 락 해제
            mutex.unlock()
        }
        tasks.add(task)
    }
 
    for (task in tasks) {
        // 메인 스레드 대기
        task.get()
    }
 
    println(count)
}

2. 재진입 가능성

동일한 스레드가 락을 여러 번 획득할 수 있는 특성

package Study
// 라이브러리 참조
import std.sync.*
import std.collection.*
import std.convert.*

class Account {
    private var balance: Float64
    private var mutex: Mutex = Mutex()
    
    init (amount: Float64) {
        this.balance = amount
    }
    
    // 예금 처리
    func deposit(amount: Float64) {
        // 락 획득
        this.mutex.lock()
        this.balance += amount
        println("입금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
        // 락 해제
        this.mutex.unlock()
    }

    // 잔액 확인
    func verify(amount: Float64) -> Bool {
        // 잔액 상태 확인
        var status = false 
        // 락 획득
        mutex.lock()
        if (this.balance >= amount) {
            status = true
        }
        // 락 해제
        this.mutex.unlock()
        return status
    }

    func withdraw(amount: Float64) {
        // 락 획득
        mutex.lock()
        if (verify(amount)) {
            this.balance -= amount
            println("출금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
        } else {
            println("잔액 부족, 잔액: ${this.balance.format(".2")}")
        }
        // 락 해제
        mutex.unlock()
    }
}

main () {
    let account = Account(0.0)

    let user1 = spawn {
        account.deposit(5000.0)
    }

    let user2 = spawn {
        account.withdraw(200.0)
    }

    user1.get()
    user2.get()
}

3. synchronized 키워드 활용

명시적인 락 관리 대신 자동 처리 기능

package Study
// 라이브러리 참조
import std.sync.*
import std.collection.*

var count = 100
// 재진입 가능한 세마포어
let mutex = Mutex()

main () {
    let tasks = ArrayList()

    // 100개의 스레드 생성
    for (_ in 0..100) {
        let task = spawn {
            // 자동 락 관리
            synchronized(mutex) {
                count++
            }
        }
        tasks.add(task)
    }

    for (task in tasks) {
        // 메인 스레드 대기
        task.get()
    }

    println(count)
}

4. 디락 예방 전략

다중 리소스 접근 시 발생하는 데드락 상황

package Study
// 라이브러리 참조
import std.sync.*
import std.collection.*
import std.convert.*
import std.time.*

class Account {
    private var balance: Float64
    
    
    init (amount: Float64) {
        this.balance = amount
    }
    
    // 예금 처리
    func deposit(amount: Float64) {
        this.balance += amount
        println("입금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
    }

    // 잔액 확인
    func verify(amount: Float64) -> Bool {
        // 잔액 상태 확인
        var status = false 
        if (this.balance >= amount) {
            status = true
        }
        return status
    }

    func withdraw(amount: Float64) {
        if (verify(amount)) {
            this.balance -= amount
            println("출금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
        } else {
            println("잔액 부족, 잔액: ${this.balance.format(".2")}")
        }
    }
}

main () {
    let acc1 = Account(3000.0)
    let acc2 = Account(1000.0)

    // 첫 번째 세마포어
    let lock1 = Mutex()
    // 두 번째 세마포어
    let lock2 = Mutex()

    // 스레드1
    let task1 = spawn {
        synchronized(lock1) {
            println("스레드1 lock1 획득")
            sleep(Duration.second)
            synchronized(lock2) {
                println("스레드1 lock2 획득")
                acc1.withdraw(300.0)
                acc2.deposit(300.0)
            }
        }
    }

    // 스레드2
    let task2 = spawn {
        synchronized(lock2) {
            println("스레드2 lock2 획득")
            sleep(Duration.second)
            synchronized(lock1) {
                println("스레드2 lock1 획득")
                acc1.deposit(300.0)
                acc2.withdraw(300.0)
            }
        }
    }

    task1.get()
    task2.get()
}

수정된 버전

package Study
// 라이브러리 참조
import std.sync.*
import std.collection.*
import std.convert.*
import std.time.*
 
class Account {
    private var balance: Float64
    
    
    init (amount: Float64) {
        this.balance = amount
    }
    
    // 예금 처리
    func deposit(amount: Float64) {
        this.balance += amount
        println("입금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
    }
 
    // 잔액 확인
    func verify(amount: Float64) -> Bool {
        // 잔액 상태 확인
        var status = false 
        if (this.balance >= amount) {
            status = true
        }
        return status
    }
 
    func withdraw(amount: Float64) {
        if (verify(amount)) {
            this.balance -= amount
            println("출금: ${amount.format(".2")}, 잔액: ${this.balance.format(".2")}")
        } else {
            println("잔액 부족, 잔액: ${this.balance.format(".2")}")
        }
    }
}
 
main () {
    let acc1 = Account(3000.0)
    let acc2 = Account(1000.0)
 
    // 첫 번째 세마포어
    let lock1 = Mutex()
    // 두 번째 세마포어
    let lock2 = Mutex()
 
    // 스레드1
    let task1 = spawn {
        synchronized(lock1) {
            println("스레드1 lock1 획득")
            sleep(Duration.second)
            synchronized(lock2) {
                println("스레드1 lock2 획득")
                acc1.withdraw(300.0)
                acc2.deposit(300.0)
            }
        }
    }
 
    // 스레드2
    let task2 = spawn {
        synchronized(lock1) {
            println("스레드2 lock1 획득")
            sleep(Duration.second)
            synchronized(lock2) {
                println("스레드2 lock2 획득")
                acc1.deposit(300.0)
                acc2.withdraw(300.0)
            }
        }
    }
 
    task1.get()
    task2.get()
}

5. 요약

이번 챕터에서는 셰지 언어의 재진입 가능한 세마포어 기능을 상세히 설명했습니다. 다음 챕터에서는 재진입 가능한 읽기/쓰기 세마포어에 대해 다룰 예정입니다.

태그: 재진입 가능한 세마포어 동기화 키워드 디락 방지 스레드 동기화 리소스 관리

7월 8일 07:43에 게시됨