[iOS] 차근차근 시작하는 GCD — 13

경쟁 상태(Race Condition) 해결 방법에 대해 알아봅시다 — Dispatch Barrier

naljin
8 min readMay 17, 2021

Previously on GCD…

ㄱㅐ-하! 오늘은 GCD 시리즈로 돌아왔습니다요.. 두세개만 더 쓰면 마무리 지을 수 있을 것 같은데 GCD 포스팅은 너무 각잡고 써야해서 시작하기 쉽지 않군여 ㅎ 튼 포기하지 말고 끝까지 가보도록 합시다

지난 시간에는 동시성에 관련된 세가지 문제 중 첫번째인 Race Condition (경쟁 상태)을 알아보았습니다. 그리고 두가지 해결 방안 중 하나를 살펴봤었죠!

  • serial queue + sync 조합의 엄격한 thread-safe 처리

오늘은 나머지 해결 방안인

  • concurrent queue + Dispatch Barrier

에 대해 다뤄보도록 하자구여

디스패치 배리어 (Dispatch Barrier)

개념

barrier 의 뜻이 모죠?!

yeah.. 그렇습니다.. 장벽! 다른 작업들 다 막어버려!

이렇게 이름에서부터 알 수 있듯, dispatch barrier는 concurrent queue가 사용하고 있는 여러 개의 thread 중에서, barrier block의 실행을 위한 하나의 스레드만 제외하고는 모든 스레드 사용을 막는 것입니다. 해당 시점에는 스레드를 하나만 사용하기 때문에 serial(직렬) 로 순서 있는 실행이 가능합니다.

concurrentQueue에 read 작업은 일반적인 task로 넣고, write 작업을 barrier task로 넣는 식으로 Dispatch barrier를 응용할 수 있습니다. 왜냐하면 읽기는 사실 여러개의 스레드에서 접근해도 상관없잖아여?? 문제가 되는건 쓰기를 통해 값이 바뀌는 경우죠…ㅂㄷㅂㄷ

Race Condition의 해결 방안으로 저번 시간에 알아본 serial queue+sync 조합과는 다르게, 이번에 알아본 방법은 concurrent queue를 사용하기 때문에 조금 더 효율적인 방법이라고 할 수 있습니다 (읽기 작업에서 여러 스레드 사용 가능)

조금 더 자세한 설명을 위해 공식 문서를 살펴보았더니 dispatch barrier는

concurrent dispatch queue에서 실행되고 있는 task 들에 대한 동기화 지점

이라고 설명하고 있네염

concurrent queue에 barrier를 추가하면, queue는 이전에 제출된 모든 작업이 실행을 마칠 때까지 barrier block (및 barrier 이후에 제출된 작업)의 실행을 지연시켰다가, 그 작업들이 끝나면 barrier block을 자체적으로 실행한다고 합니당

사용법

😩 : 아 개념은 알겠고 어케 쓰는거냐고~~~

dispatch queue 의 인스턴스 메소드인 async(group:qos:flags:execute:)를 사용해서 flags 파라미터에 .barrier 를 넣어주도록 합시다.

바로 바로 이렇게~~!

concurrentQueue.async(flags: .barrier, execute: { })

😡 : 으아니~~ 좀 더 구체적인 코드를 가져와라!!

ㅎㅎ.. 넘 길다 길어.. 찬찬히 살펴봅시다..

["수지니", "날지니", "해동청", "보라매"] 로 각각 이름을 바꾸고 읽어오는 작업을 workQueue라는 concurrent 에 던지고 있는데요..!!

그림으로 보면 이렇게 되겠쥬?

이때 Write / Read 로 접근하는 객체가 falcon이라는 shared instance 이기 때문에 여러 스레드에서 값을 변경하려고 하는 경쟁 상황 방지를 해줘야합니다.

참고로 수지니, 날지니, 해동청, 보라매 모두 매의 종류라서 falcon(매) 으로 instance를 생성했답니당.

위의 코드에서 경쟁 상황을 방지하기 위해 우리는 concurrent queue를 만들고 write 작업에 대해선 dispatch barrier 를 사용했습니다. 이를 통해 값 변경이 동시에 발생하는 것을 방지할 수 있습니다.

여기서 read 작업은 정확한 값을 불러와야하기 때문에 sync를 사용하고 (읽어라 명령만 내리고 바로 내 context로 돌아오면 안됨), write는 변경해라! 명령만 내리고 내 context로 돌아와도 문제가 없기 때문에 async 를 사용합니다.

이 상황을 그림으로 보면 이렇게 되겠쥬?

위의 그림처럼 순차적으로 실행이 된다면 출력도 예상대로 예쁘게 잘 찍힙니다.

name is : 수지니
name is : 날지니
name is : 해동청
name is : 보라매

하지만 아래처럼 같은 이름이 중복으로 찍힐때도 있는데요?!

name is : 해동청
name is : 해동청
name is : 해동청
name is : 보라매

후훟….. 이전 포스팅에서도 비슷한 상황을 본적이 있져

🤔 : ??????????? 머야 이거 thread safe 한거 맞아?? 값이 왜 이따ㅇ….

 : 응~ Tsan 에는 안걸려~

맞아여.. Tsan 에 안걸린다는건 Thread Safe 한 코드라는 얘기이져.. 그럼 왜 이런 상황이 발생하냐!!!

바로 바로 요런식으로 다른 곳의 write 가 먼저 보내지는 상황에서 발생할 수 있습니다

그러니까~~ 덮어 씌워진 값을 프린트 하고 싶지 않다면 print 를 write 하는 task 밑에 같이 넣는다던지 하자구요..? 이것도 좀 이상한거 같긴하지만..???!!?!?? 튼 요지는 객체 설계를 첨부터 의도에 맞게 잘하자!는 것입니다.

var name: String {
get {
self._name
}
set {
concurrentQueue.async(flags: .barrier, execute: {
self._name = newValue
print(self._name)
})
}
}

마무리

이제 우리는 위의 예시에서 살펴봤던 것 처럼

  • 객체에 접근 (ex. falcon) 할 때,
  • 메인 큐(쓰레드)가 아닌 다른 큐에서 접근할 가능성(ex. workerQueue에서 접근) 이 있는가? 를 항상 생각하고,
  • 객체 내부에 thread safe한 코드 작성(serial queue + sync 혹은 Dispatch barrier) 할 수 있어야 합니다.

이전 포스팅에 걸쳐 지금까지 Race Condition 즉, 2개 이상의 스레드에서 공유된 자원에 동시적으로 접근하는 상황인지 확인 하는 방법(Tsan)과 두가지 해결 방법(serial queue + sync, Dispatch barrier) 을 알아보았습니다.

추가적으로 세마포어를 이용한 해결방법도 있다는데.. 이건 쓸까 말까 고민중이에여 ㅎ 일단 이 정도만 알아도 괜찮을 것 같습니당

역시 GCD 포스팅은 넘나뤼 넘나 힘든것.. 언제가 될진 모르겄지만 다음 시간에는 교착 상태(Deadlock) 에 대한 주제로 돌아오겠어여!!!!!!! 한달에 한번은 GCD 시리즈 포스팅하기가 목표니까 6월달 안에는 돌아오겠죠?ㅎㅎㅎㅎㅎ

긴 글 읽느라 고생하셨습니다! 그럼 20000!

이전 포스팅 👈🏻

다음 포스팅 👉🏻

출처

--

--