예금(deposit)과 대출(loan)은 각각 업무마다 필요한 시간이 상이합니다. 업무 시간을 부여하기 위해 Thread.sleep()을 사용하였는데, 이 부분은 글이 길어질 것을 고려하여 생략하였습니다.
프로젝트를 진행하면서, Race Condition을 막기 위해 DispatichSemaphore
를 사용해보았습니다.
여기서 혼란이 생기더라고요..? 비동기 작업에 들어가기 전에 Thread 수를 제한
하는 것(방법 1)이 논리적으로 맞게 느껴지는데, 실제로 원하는 결과와 유사한 건 DispatchQueue에 접근하는 Thread 수를 제한
하는 것(방법 2)였습니다.
⬇️ 방법 1: async 블록 외부에 semaphore.wait()이 위치
private let depositSemaphore = DispatchSemaphore(value: 2)
private let loanSemaphore = DispatchSemaphore(value: 1)
private func assignWork() {
while let customer = bank?.waitingLine.dequeue() {
switch customer.requestedWork {
case .deposit:
self.depositSemaphore.wait() // 이 부분
DispatchQueue.global().async(group: employeeGroup) {
self.employee.work(for: customer)
self.depositSemaphore.signal()
}
case .loan:
self.loanSemaphore.wait() // 이 부분
DispatchQueue.global().async(group: employeeGroup) {
self.employee.work(for: customer)
self.loanSemaphore.signal()
}
}
}
}
⬇️ 방법 2: async 블록 내부에 semaphore.wait()이 위치
private let depositSemaphore = DispatchSemaphore(value: 2)
private let loanSemaphore = DispatchSemaphore(value: 1)
private func assignWork() {
while let customer = bank?.waitingLine.dequeue() {
switch customer.requestedWork {
case .deposit:
DispatchQueue.global().async(group: employeeGroup) {
self.depositSemaphore.wait() // 이 부분
self.employee.work(for: customer)
self.depositSemaphore.signal()
}
case .loan:
DispatchQueue.global().async(group: employeeGroup) {
self.loanSemaphore.wait() // 이 부분
self.employee.work(for: customer)
self.loanSemaphore.signal()
}
}
}
}
방법 1
의 출력을 보면, 업무의 시작이 모두 오름차순입니다.
반면 방법 2
는 그런 규칙성이 없어 보입니다. 왜일까요?🤔
DispatchSemaphore는 입력된 value 값만큼 Thread 수에 제한을 주는 방식으로 동작합니다. 그렇기 때문에 제가 위에서 말한 대로,
방법 1
은 async block이 언제나 딱 두 개
만 등록되는 반면,
방법 2
는 async block이 모두 등록
된 후, DispatchQueue에 접근하기 위해 작업들이 대기
하는 모습을 띄게 됩니다.
Thread의 관점에서 본다면,
방법 1
은 동시에 가동되는 Thread 수가 제한되고 있으며,
방법 2
는 GCD가 판단한, 작업에 필요한 만큼의 Thread가 생성되어 차례를 기다리는 모습으로 볼 수 있겠네요.
이런 차이때문에 전자는 오름차순으로 업무가 시작되는 규칙성을 가지게 됩니다. 조금 더 시각적인 설명을 위해 switch문 외부에 print문을 적어보았습니다.
⬇️ 방법 1
의 작업 등록 과정
⬇️ 방법 2
의 작업 등록 과정
위에서 이야기했던 두 방법의 동작 차이가 잘 드러나는 결과가 나왔습니다. 정말... 재밌지 않나요?
저는 이 두가지를 각각 고객 이탈 가능여부의 유무로 나누어 사용해보면 좋겠다는 생각을 하게되었고, 결과적으로 이번 프로젝트에서는 후자를 차용하게 되었어요.
이 글을 읽고 계신 분들은 각각의 케이스에 따라 모델이 어떻게 달라질 것이라 생각하시나요? 저는 맘같아서는 은행 문 열고 들어온 고객은 못 나가도록 하고싶네요...
🥳 이번 실험에 굉장한 도움을 주신 리뷰어 찰리에게 감사의 말씀 드립니다! 찰리의 말을 들으면 자다가도 떡이 생긴다!
아래는 리뷰어 찰리가 Timer에 대한 추가적인 의견을 공유해주셔서 메모한 내용입니다.
제가 아직은 이해할 수 없는 부분이 있어서 이후에 확인하고 doDeep할 수 있도록 메모하는 용도니까 읽지 않으셔도 완전 무방합니다
찰리가 현업에서 해당 프로젝트를 진행한다면, MultingThreading을 사용하지 않고 Timer
를 두었을 것 같다는 이야기를 나눠주었다. Timer의 종료 시점을 업무의 끝으로 보고, 업무가 끝나는 대로 Queue에서 대기고객을 꺼내면 다시 Timer가 작동되는 방식을 말씀하셨는데, 직원 객체 2개가 Timer를 들고 있는 이 경우, Race Condition도 발생하지 않는 데다가 등록과 실행이 동시에 되기 때문에 실제 업무 시간이 더 짧게 악덕고용주 나올 수 있다고 한다.
Timer가 global에서 되지 않느냐는 질문의 답으로 왜 Runloop에 Timer를 등록하여야 하는지 알아오는지 알아보도록 떡을 내려주셨다.
🙂 대단합니다. 그런데 이 소식은 누구에게 이롭습니까? 하지만 멋있다.🤭😎