[Swift] Concurrency Programming - 4

Martin Kim·2022년 2월 10일
0

swift-concurrency

목록 보기
4/4

동시성 프로그래밍의 Race Condition Issue

  • 동시성 프로그래밍은 강력한 기능이지만 Race Condition Issue가 발생할 수 있다.

    import Foundation
    
    var cards = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    DispatchQueue.global().async {
        for _ in 1...3 {
            let card = cards.removeFirst()
            print("야곰: \(card) 카드를 뽑았습니다!")
        }
    }
    
    DispatchQueue.global().async {
        for _ in 1...3 {
            let card = cards.removeFirst()
            print("노루: \(card) 카드를 뽑았습니다!")
        }
    }
    
    DispatchQueue.global().async {
        for _ in 1...3 {
            let card = cards.removeFirst()
            print("오동나무: \(card) 카드를 뽑았습니다!")
        }
    }
    
    /* 출력예시
    야곰: 1 카드를 뽑았습니다!
    노루: 1 카드를 뽑았습니다! // 1 카드는 1장이지만 여러번 뽑히고 있다?!
    오동나무: 1 카드를 뽑았습니다!
    야곰: 2 카드를 뽑았습니다!
    노루: 5 카드를 뽑았습니다!
    야곰: 6 카드를 뽑았습니다!
    노루: 8 카드를 뽑았습니다!
    오동나무: 7 카드를 뽑았습니다!
    오동나무: 9 카드를 뽑았습니다!
    */
    • 위와 같은 예제의 오류가 발생하는 이유는 하나의 자원, 값에 여러 스레드가 접근하여 동시에 작업하기 때문이다.

DispatchSemaphore를 사용하여 해결하기

  • 공유 자원에 접근할 수 있는 스레드의 수를 제한할 수 있다.
  • ARC랑 비슷하게?? semaphore count를 증가시키고 감소시키는 원리로 동작
  • 예를 들어 하나의 스레드가 접근을 하면 count에 -1을, 접근이 끝나면 count에 +1
  • 따라서 설정해준 count만큼만 스레드가 접근할 수 있도록 한다
  • 만약 허용된 스레드의 수만큼 접근된 상태라면 다른 스레드는 접근하지 못하고 줄을 서서 기다리게 한다.
let semaphore = DispatchSemaphore(value: 1) // count = 1

DispatchQueue.global().async {
    semaphore.wait() // count -= 1 반드시 wait과 signal은 짝지어서 실행시켜야 한다.
    semaphore.signal() // count += 1
}

이를 통해 위 예제의 문제를 해결하는 코드

import Foundation

var cards = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let semaphore = DispatchSemaphore(value: 1)

DispatchQueue.global().async {
    for _ in 1...3 {
        semaphore.wait()
        let card = cards.removeFirst()
        print("야곰: \(card) 카드를 뽑았습니다!")
        semaphore.signal()
    }
}

DispatchQueue.global().async {
    for _ in 1...3 {
        semaphore.wait()
        let card = cards.removeFirst()
        print("노루: \(card) 카드를 뽑았습니다!")
        semaphore.signal()
    }
}

DispatchQueue.global().async {
    for _ in 1...3 {
        semaphore.wait()
        let card = cards.removeFirst()
        print("오동나무: \(card) 카드를 뽑았습니다!")
        semaphore.signal()
    }
}
  • DispatchSemaphore는 스레드의 접근을 제어하기 위한 수단이며 꼭 Race Condtion을 해결하기 위한 수단만은 아니다.

Serial Queue를 사용하여 해결하기

  • Race Condition 이슈가 발생하는 이유는 질서없이 자원에 접근했기 때문이며 이를 해결하기 위해서는 질서가 있는 Serial Queue를 사용하는 트릭을 사용하면 된다.
  • Serial Queue가 하나의 스레드만을 사용하기 때문
import Foundation

var cards = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let pickCardsSerialQueue = DispatchQueue(label: "PickCardsQueue")

DispatchQueue.global().async {
    for _ in 1...3 {
        pickCardsSerialQueue.sync {
            let card = cards.removeFirst()
            print("야곰: \(card) 카드를 뽑았습니다!")
        }
    }
}

DispatchQueue.global().async {
    for _ in 1...3 {
        pickCardsSerialQueue.sync {
            let card = cards.removeFirst()
            print("노루: \(card) 카드를 뽑았습니다!")
        }
    }
}

DispatchQueue.global().async {
    for _ in 1...3 {
        pickCardsSerialQueue.sync {
            let card = cards.removeFirst()
            print("오동나무: \(card) 카드를 뽑았습니다!")
        }
    }
}

UI 작업을 main thread에서 해주어야 하는 이유

결론은 Race Condition issue로 인한 것이다.

  1. UIKit는 Thread safe하지 않다. -> 즉 Race condition issue가 발생할 수 있다.
  2. 메인 스레드에는 Main RunLoop가 동작하고 있어서 일정한 주기로 사용자의 입력을 받아 UI를 그리게 된다.
    • 이를 View Drawing Cycle이라고 한다. 사실 모든 스레드는 각자의 Runloop를 가지게 되는데 만약 RunLoop에 따라 각자가 UI를 그리게 된다면 UI가 그려지는 시점이 모두 제각각이 되기 때문이다. 이렇게 되면 Race condition이 발생할 수 있다.
    • 이것을 Main RunLoop의 기준에 맞추는 것으로 해결할 수 있다.

출처: 야곰닷넷 동시성 프로그래밍 강좌

profile
학생입니다

0개의 댓글