포스트

Async/Await (11)

Bank Account Playground 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BankAccount {
    
    var balance: Double
    
    init(balance: Double) {
        self.balance = balance
    }
    
    func withdraw(_ amount: Double) {
        
        if balance >= amount {
            
            let processingTime = UInt32.random(in: 0...3)
            print("[Withdraw] Processing for \(amount) \(processingTime) seconds")
            sleep(processingTime)
            print("Withdrawing \(amount) from account")
            balance -= amount
            print("Balance is \(balance)")
        }
        
    }
    
}

다음과 같이 간단하게 만들어 준다.

출력하면

1
2
3
4
5
6
7
8
9
10
let bankAccount = BankAccount(balance: 500)
bankAccount.withdraw(300)
print(bankAccount.balance)

/*
[Withdraw] Processing for 300.0 1 seconds
Withdrawing 300.0 from account
Balance is 200.0
200.0
*/

이렇게 결과가 나온다.

1. Concurrent Queue의 문제점

1
2
3
4
5
6
7
8
9
let queue = DispatchQueue(label: "ConcurrentQueue", attributes: .concurrent)

queue.async {
    bankAccount.withdraw(300)
}

queue.async {
    bankAccount.withdraw(500)
}

동시에 실행가능하게 구성을하고 출력을 해보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[Withdraw] Processing for 500.0 3 seconds
[Withdraw] Processing for 300.0 2 seconds
Withdrawing 300.0 from account
Balance is 200.0
Withdrawing 500.0 from account
Balance is -300.0

또는

[Withdraw] Processing for 300.0 0 seconds
Withdrawing 300.0 from account
Balance is 200.0
[Withdraw] Processing for 500.0 1 seconds
Withdrawing 500.0 from account
Balance is -300.0

또는

[Withdraw] Processing for 300.0 2 seconds
[Withdraw] Processing for 500.0 0 seconds
Withdrawing 500.0 from account
Balance is 0.0
Withdrawing 300.0 from account
Balance is -300.0

실행할때마다 다른 결과를 가져오는데 문제는 그게 아니라

위에있는 if balance >= amount 의 조건이 맞지 않는데도 작동했다는것이다.

이렇게 동시에 작동하는 queue경우 이런 문제가 생길수가 있다.

이런 문제가 발생하는 이유는 병렬로 실행할 당시

이미 if 조건을 둘다 통과한 상태로 시작하기 때문.

2. Serial Queue를 사용한 문제 해결

let otherQueue = DispatchQueue(label: "SerialQueue") 이렇게 attributes를 생략하면 Serial Queue로 만들어 진다.

1
2
3
4
5
6
7
otherQueue.async {
    bankAccount.withdraw(300)
}

otherQueue.async {
    bankAccount.withdraw(500)
}

똑같이 조건을 만들고 실행하면

1
2
3
[Withdraw] Processing for 300.0 0 seconds
Withdrawing 300.0 from account
Balance is 200.0

이젠 if 조건에 걸리면서 잔액을 초과하는 금액이 인출되지 않게 된다.

3. Locks를 사용한 문제 해결

이번엔 Concurrent Queue을 그대로 사용한다.

Lock을 사용하기위해선 Lock Instance를 만들어 줘야한다.

let lock = NSLock()

그리고 다음과 같이 사용해준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func withdraw(_ amount: Double) {
    
    lock.lock()
    if balance >= amount {
        
        let processingTime = UInt32.random(in: 0...3)
        print("[Withdraw] Processing for \(amount) \(processingTime) seconds")
        sleep(processingTime)
        print("Withdrawing \(amount) from account")
        balance -= amount
        print("Balance is \(balance)")
    }
    lock.unlock()
    
}

그리고 실행을하면

1
2
3
[Withdraw] Processing for 300.0 2 seconds
Withdrawing 300.0 from account
Balance is 200.0

이렇게 출력이 된다.

Lock은 await처럼 Lock이 시작되는 시점에서 한작업이 끝날때 까지 Lock이 걸린 부분부터 작업을 중단하고 기다리게 해준다.

즉, 동시에 Queue가 실행되어도, 먼저 작성한 Queue가 우선적으로 실행이 되고, 그 작업이 끝난후 다음 Queue가 실행.

몇번을 재실행해도 순서가 변하지 않고 300을 먼저 출금하게 된다.

해당기능은 iOS 14이하 버전에서 사용된 방법이라고한다.

그리고 Lock의 경우 반드시 unlock을 해줘야한다.

Lock을 사용한 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
queue.async {
    bankAccount.withdraw(300)
}

queue.async {
    bankAccount.withdraw(100)
}

/*
[Withdraw] Processing for 300.0 0 seconds
Withdrawing 300.0 from account
Balance is 200.0
[Withdraw] Processing for 100.0 2 seconds
Withdrawing 100.0 from account
Balance is 100.0
*/

Lock을 사용하지 않은 경우

1
2
3
[Withdraw] Processing for 300.0 0 seconds
Withdrawing 300.0 from account
Balance is 200.0

그 이후로 진행이 되지 않는다. 여전히 해당 thread에서 Queue가 끝나기를 기다리는중.

즉, lock()을 호출한 후 unlock()을 호출하지 않으면 Lock이 계속 유지되어 다른 쓰레드나 작업이 해당 Lock에 접근할 수 없게 된다. 이로 인해 Deadlock(교착 상태) 이 발생할 수 있으며, 프로그램이 멈추거나 비정상적으로 동작하게 된다.

마치 전에 DispatchGroup을 사용할때 enter를 해서 시작하고, leave를 통해 해당 queue에 대해 마무리를 해주는 느낌이랄까.

정리를 하던 도중 Lock에 관한 예시를 GPT를 통해 확인하던중 좋은 리소스를 주어 적어본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
func withdraw(_ amount: Double) {
    lock.lock()
    defer { lock.unlock() } // Lock 해제 보장

    if balance >= amount {
        let processingTime = UInt32.random(in: 0...3)
        print("[Withdraw] Processing for \(amount) \(processingTime) seconds")
        sleep(processingTime)
        print("Withdrawing \(amount) from account")
        balance -= amount
        print("Balance is \(balance)")
    }
}

이런식으로 이전에 언급한 defer를 사용하는것도 꽤나 좋은 방법인듯하다.

이렇게되면 처음에

1
2
3
4
5
6
7
queue.async {
    bankAccount.withdraw(300)
}

queue.async {
    bankAccount.withdraw(100)
}

defer를 사용하면 첫 withdraw함수가 끝나면 디퍼가 실행되면서 unlock이 실행된다.

이해를 돕기위해

1
2
3
defer {
    print("Defer Activated")
    lock.unlock() } 

print를 통해 디퍼가 작동되는 시점을 확인.

1
2
3
4
5
6
7
8
[Withdraw] Processing for 300.0 3 seconds
Withdrawing 300.0 from account
Balance is 200.0
Defer Activated
[Withdraw] Processing for 100.0 0 seconds
Withdrawing 100.0 from account
Balance is 100.0
Defer Activated

이렇게 나온다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.