원문
뜻을 이해하기 쉽도록 의역/추가설명을 붙인 부분들이 있습니다.
Question
Swift 멀티 쓰레드 환경에서 Array에 아이템을 추가하려고 했더니 이슈가 발생했습니다(Array가 쓰레드에 안전하지 않음). 어떻게 하면 될까요?
저는 블럭을 저장하는 Array를 만들고, 요청을 받았을 때 그 Array 안의 블럭을 전부 실행시켜주고 싶었습니다. 그래서 다음과 유사한 코드를 작성했습니다.
class MyArrayBlockClass {
private var blocksArray: Array<() -> Void> = Array()
private let blocksQueue: NSOperationQueue()
func addBlockToArray(block: () -> Void) {
self.blocksArray.append(block)
}
func runBlocksInArray() {
for block in self.blocksArray {
let operation = NSBlockOperation(block: block)
self.blocksQueue.addOperation(operation)
}
self.blocksQueue.removeAll(keepCapacity: false)
}
}
문제는 addBlockToArray 함수가 여러 쓰레드에서 호출되며 발생했습니다. 서로 다른 쓰레드들에서 이 함수가 연속적으로 빠르게 호출되었을 때, 파라미터로 넘어온 블럭 중 오직 하나만이 배열에 추가되었습니다. 그래서 runBlocksInArray 함수가 호출되었을 때 다른 블럭들은 실행되지 않았습니다.
아래의 시도도 해보았지만 소용없었습니다.
private let blocksDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
func addBlockToArray(block: () -> Void) {
dispatch_async(blocksDispatchQueue) {
self.blocksArray.append(block)
}
}
Answer
질문의 코드에서는 blockDispatchQueue를 global queue로 정의하고 있습니다. 이 코드를 Swift3의 문법으로 다시 작성해보면, 이렇게 됩니다.
private let queue = DispatchQueue.global()
func addBlockToArray(block: @escaping () -> Void) {
queue.async {
self.blocksArray.append(block)
}
}
문제는 global queue가 concurrent queue라는 것입니다.(역주: 동시에 task가 실행되는 queue. Serial queue와 반대) 따라서 동기화(synchronization)가 되지 않습니다. 이럴 때는 자신만의 custom serial queue를 만들어 사용하면 됩니다. Swift3에서는 이런 식으로 만들 수 있습니다.
private let queue = DispatchQueue(label: "com.domain.app.blocks")
이런 custom queue는 기본적으로 serial queue입니다. 그러므로, 동기화가 가능하게 됩니다.
참고로, blocksQueue와의 인터랙션을 동기화하기 위해 이 blockDispatchQueue를 사용할 경우, blocksArray와의 모든 인터랙션은 이 queue를 통해 이루어져야 합니다. 예를 들어 operation을 blocksQueue에 추가할 때도 같은 queue안에서 이루어져야 합니다.
func runBlocksInArray() {
queue.async {
for block in self.blocksArray {
let operation = BlockOperation(block: block)
self.blocksQueue.addOperation(operation)
}
self.blocksArray.removeAll()
}
}
Reader/writer 패턴을 사용하는 방법도 있습니다.(역주: Read-Write Lock 패턴: Read/Write 시마다 계속 Lock을 걸면 성능이 떨어지므로, Read만이 동시에 이루어지는 것은 허용해도 되지 않을까, 라는 패턴. 자세한 것은 위키 참고.) 자신만의 concurrent queue를 만드세요.
private let queue = DispatchQueue(label: "com.domain.app.blocks", attributes: .concurrent)
Reader-writer 패턴에서 쓰기 동작은 barrier 속성(쓰기가 순차적으로 수행될 수 있게 지켜주는 속성)으로 수행되어야 합니다.
func addBlockToArray(block: @escaping () -> Void) {
queue.async(flags: .barrier) {
self.blocksArray.append(block)
}
}
그러나 데이터를 읽는 것은 그냥 이렇게 할 수 있습니다.
let foo = queue.sync {
blocksArray[index]
}
이 패턴의 장점은 쓰기 동작을 동기화할 수 있으며, 읽는 동작은 여러 곳에서 동시에 가능하다는 것입니다. 이 케이스만 두고 보면 그런 성능향상이 꼭 필요하지 않을 수도 있지만 (그래서 심플한 serial queue로도 충분할 수 있지만) 완벽을 기하기 위해 이 패턴을 답변에 포함했습니다.
또 다른 방법으로는 NSLock이 있습니다.
extension NSLocking {
func withCriticalSection<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}
}
이렇게 적용합니다.
let lock = NSLock()
func addBlockToArray(block: @escaping () -> Void) {
lock.withCriticalSection {
blocksArray.append(block)
}
}
읽는 동작은 이렇게 할 수 있습니다.
let foo = lock.withCriticalSection {
blocksArray[index]
}
NSLock은 그동안 항상 성능이 좋지 않다고 무시되었지만, 요즘은 심지어 GCD보다 빠릅니다.
'iOS 일반 > Stack Overflow' 카테고리의 다른 글
DispatchQueue.main.async 와 DispatchQueue.main.sync 의 차이점 (Stack Overflow) (1) | 2019.12.04 |
---|