티스토리 뷰

원문

Adding items to Swift array across multiple threads causing issues (because arrays aren't thread safe) - how do I get around that?

뜻을 이해하기 쉽도록 의역/추가설명을 붙인 부분들이 있습니다.


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보다 빠릅니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함