[swift] semaphore.wait()의 위치에 따른 작동 차이

GOSARI·2021년 12월 31일
2

swift

목록 보기
11/11

아래 코드를 이해하기 위한 간단 설명

예금(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 수가 제한되고 있으며,
방법 2GCD가 판단한, 작업에 필요한 만큼의 Thread가 생성되어 차례를 기다리는 모습으로 볼 수 있겠네요.

이런 차이때문에 전자는 오름차순으로 업무가 시작되는 규칙성을 가지게 됩니다. 조금 더 시각적인 설명을 위해 switch문 외부에 print문을 적어보았습니다.

⬇️ 방법 1의 작업 등록 과정

⬇️ 방법 2의 작업 등록 과정

위에서 이야기했던 두 방법의 동작 차이가 잘 드러나는 결과가 나왔습니다. 정말... 재밌지 않나요?

저는 이 두가지를 각각 고객 이탈 가능여부의 유무로 나누어 사용해보면 좋겠다는 생각을 하게되었고, 결과적으로 이번 프로젝트에서는 후자를 차용하게 되었어요.

이 글을 읽고 계신 분들은 각각의 케이스에 따라 모델이 어떻게 달라질 것이라 생각하시나요? 저는 맘같아서는 은행 문 열고 들어온 고객은 못 나가도록 하고싶네요...

그래서 글 어떻게 끝내야 하죠??


🥳 이번 실험에 굉장한 도움을 주신 리뷰어 찰리에게 감사의 말씀 드립니다! 찰리의 말을 들으면 자다가도 떡이 생긴다!


아래는 리뷰어 찰리가 Timer에 대한 추가적인 의견을 공유해주셔서 메모한 내용입니다.

제가 아직은 이해할 수 없는 부분이 있어서 이후에 확인하고 doDeep할 수 있도록 메모하는 용도니까 읽지 않으셔도 완전 무방합니다

찰리가 현업에서 해당 프로젝트를 진행한다면, MultingThreading을 사용하지 않고 Timer를 두었을 것 같다는 이야기를 나눠주었다. Timer의 종료 시점을 업무의 끝으로 보고, 업무가 끝나는 대로 Queue에서 대기고객을 꺼내면 다시 Timer가 작동되는 방식을 말씀하셨는데, 직원 객체 2개가 Timer를 들고 있는 이 경우, Race Condition도 발생하지 않는 데다가 등록과 실행이 동시에 되기 때문에 실제 업무 시간이 더 짧게 악덕고용주 나올 수 있다고 한다.

Timer가 global에서 되지 않느냐는 질문의 답으로 왜 Runloop에 Timer를 등록하여야 하는지 알아오는지 알아보도록 떡을 내려주셨다.

RunLoop 참고링크

ios) 런 루프(RunLoop) 이해하기

2개의 댓글

comment-user-thumbnail
2021년 12월 31일

🙂 대단합니다. 그런데 이 소식은 누구에게 이롭습니까? 하지만 멋있다.🤭😎

1개의 답글