Multi Thread 에서 접근하는 안전한 Property 설계 GCD (DispatchQueue)
private let accessQueue = DispatchQueue(label: "SynchronizedAccess",
attributes: .concurrent)
private var _someValue: [Int]
var someValue: [Int] {
get {
var someValue: [Int] = []
accessQueue.sync {
someValue = _someValue
}
return someValue
}
set {
accessQueue.async(flags: .barrier) {
self.willChangeValue(for: \.someValue)
self._someValue = newValue
self.didChangeValue(for: \.someValue)
}
}
}
코드 설명,
accessQueue 생성할 때 .concurrent 를 명시했다.
Serial Queue를 사용할 경우에 요청한 순서대로 실행되므로,
서로 다른 쓰레드에서 동시에 접근한 경우 뒤에 읽기 동작이 앞의 읽기 동작이 끝날 때까지 대기하게 되어
UX가 멈칫하는 등의 성능저하가 발생할 수 있다.
getter 읽기 동작시에는 sync 로 값을 읽고 리턴한다.
이 때 sync 로 호출했지만, .concurrent 속성으로 인해서 서로 다른 쓰레드의 동시 접근도 동시에 실행가능하다.
setter 쓰기 동작시에는 async 를 사용했지만, .barrier 플래그를 활용해
이전의 모든 sync 읽기 동작이 끝난 후에 배타적으로(이후에 요청된 다른 동작을 대기시키고,) 값을 설정하게 된다.
위 기법은 멀티프로세서에서 동시에 서로 다른 CPU 에서 읽기와 쓰기가 발생하면서,
성능이 저하되거나 잘못된 값을 읽는 문제도 해결하고 있다.
변경하는 동안에는 하나의 쓰레드에서만 저장소에 접근하므로,
동시에 서로 다른 CPU 에서 쓰기+쓰기, 쓰기+읽기 모두 발생할 수 없다.
이것은 쓰기 동작이 끝난 후에 CPU.cache 가 갱신되므로,
이후에는 서로 다른 CPU 에서 읽기를 동시에 처리하더라도 잘못된 값을 읽지 않는다.
주의
accessQueue 가 외부에 노출되어서는 곤란하다.
someValue getter 에서 sync 로 요청되므로, 외부에서 accessQueue.sync 블록 내부에서
someValue getter 를 호출할 경우에 크래시가 발생할 수 있고,
someValue setter 에서 .barrier 를 사용하므로, 외부에서 accessQueue sync/async 블록 내부에서
someValue setter 를 호출할 경우 교착상태에 빠져 현재 처리 Thread 와 DispatchQueue 모두 멈춘다.
참고
* accessQueue.async(flags: .barrier) {} 에 대해서.
동시에 요청된 동작들이 끝날 때까지 기다려서 .barrier 속성을 가진 DispatchWorkItem 이 실행된다.
또한 .barrier 속성의 DispatchWorkItem 요청 이후의 처리는 .barrier 속성의 DispatchWorkItem 이 끝날 때 까지 대기 한 후에 실행된다.
* DispatchQueue 와 Thread 의 관계
DispatchQueue 와 Thread 를 동일시 해서는 곤란하다.
DispatchQueue 가 매번 동일한 Thread 에서 실행되지 않고, 종종 Thread 가 변경된다.
DispatchQueue 에 workItem 처리 요청이 들어오면, 이 때 Thread 가 지정되어 실행하게 된다.
즉 DispatchQueue 를 백만개 만들더라도, Thread 의 최대 개수는 OS 에 의해서 제한되고,
각각의 DispatchQueue 와 Thread 스케줄 역시 OS가 알아서 해준다.
- 사실 DispatchQueue 의 최대 개수가 제한되어 있다고 알려져 있다.