티스토리 뷰

Swift

[Swift] Race Condition과 Thread Safe

Esiwon 2022. 12. 12. 23:00

비동기적으로 기능을 구현하다 보면, 순차적으로 코드가 실행되는 것이 아니다 보니 주의해야 할 점이 생긴다. 그중 Race Condition과 해결책인 Thread Safe에 대해 알아보고자 한다.

Race Condition

Race Condition 이란, 여러 Thread가 하나의 자원에 동시에 접근해서 자원이 변질되어 원하는 결과가 나오지 않는 현상을 뜻한다.

 

비동기적으로 코드가 실행되면, 여러 Thread 동시에 하나의 자원에 접근하는 경우가 생긴다. 이때 Thread가 한 자원 접근해 해당 자원에 변화를 주게 되면, Race Condition 발생되고, 원하는 결과를 얻기 힘들어진다.

 

코드를 통해 이해해 보자.

var books = ["어린왕자", "백설공주", "해리포터", "신데렐라", "알라딘", "라푼젤"]
DispatchQueue.global().async {
    for _ in 1...2 {
        let book = books.removeFirst()
        print("손님1: \(book)을 샀습니다.")
    }
}
DispatchQueue.global().async {
    for _ in 1...2 {
        let book = books.removeFirst()
        print("손님2: \(book)을 샀습니다.")
    }
}
DispatchQueue.global().async {
    for _ in 1...2 {
        let book = books.removeFirst()
        print("손님3: \(book)을 샀습니다.")
    }
}
// 손님2: 어린왕자을 샀습니다.
// 손님3: 어린왕자을 샀습니다.
// 손님1: 어린왕자을 샀습니다.
// 손님1: 백설공주을 샀습니다.
// 손님2: 신데렐라을 샀습니다.
// 손님3: 신데렐라을 샀습니다.

위와 같이 books라는 하나의 자원에 3개의 Thread가 동시에 접근해 같은 책이 여러번 print되는 버그가 발생한다. 위 같은 경우를 Race Condition라고 한다.

 

위 같은 문제를 해결해 주기 위해 Thread Safe가 필요하다.

Thread Safe

Thread Safe란, 여러 Thread가 한 자원에 동시에 접근하는 것을 막음으로 써 "정확성"을 보장하는 것,
기본적으로 Swift의 대부분의 타입들은 여러 Thread가 동시에 접근 가능한 Thread Unsafe의 특정을 가지고 있다.

 

Thread Safe하게 하기 위해서

그림과 같이 작업을 줄 세우기 한다고, 생각하면 쉬울 것 같다.
쉽게 생각해 해당 자원에 어떠한 Thread가 접근 중이라면, 다른 Thread는 기다면 된다.

 

자 그럼 작업을 줄 세우기 할 수 있는 방법, 즉 Thread Safe 하게 하는 방법은 다음과 같다.

  • DispatchSemaphore
  • Custom Serial Queue
  • NSLock
  • Dispatch Barrier

그럼 하나씩 알아보쟈..!!

DispatchSemaphore

DispatchSemaphorecounting semaphore 방식으로 자원에 접근 가능한 쓰레드를 수적으로 제한하는 방식 때문에 1개의 쓰레드만 접근을 허용하면, Race Condition를 해결할 수 있다. 자원에 접근하기 전 한 쓰레드만 들어갈 수 있는 방을 만들어주는 느낌?

let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
    for _ in 1...2 {
        semaphore.wait()
        let book = books.removeFirst()
        print("손님1: \(book)을 샀습니다.")
        semaphore.signal()
    }
}

위 코드와 같이 1개의 쓰레드만 접근 가능한DispatchSemaphore인스턴스를 만들어주고, 하나의 자원에 접근하기 전에 wait()을 호출에 줌으로써 다른 쓰레드는 지금 접근 자원에 접근 중인 쓰레드가 자원을 사용하고 비켜주길 기다린다. 자원에 접근해서 작업이 끝나면, signal()를 호출해 다른 쓰레드가 자원에 접근할 수 있게 한다.

Custom Serial Queue

Custom Serial Queue를 이용하여 작업을 줄세우면, DispatchSemaphore와 마찬가지로 자원에 한 쓰레드만 접근 하게할 수 있다.

DispatchQueue.global().async {
    for _ in 1...2 {
        serialQueue.sync {
            let book = books.removeFirst()
            print("손님1: \(book)을 샀습니다.")
        }
    }
}

위와 같이 global쓰레드에서 serialQueue로 작업을 보내면, serialQueue에서 순차적으로 들어온 작업을 실행한다.

DispatchQueue.global().async

위 코드를 통해

위 그림과 같이 각 쓰레드는 하나의 작업을 실행하게 된다.

serialQueue.sync {
    let book = books.removeFirst()
    print("손님1: \(book)을 샀습니다.")
}

근데 쓰레드 내부에서 위 코드를 실행하게 되면,

위와 같이 또 각 쓰레드는 작업을 해당 serialQueue에 예약하게 된다.

그러면,

위와 같이 serialQueue는 직렬이기 때문에 먼저 들어온 작업 부터 실행하게 된다. 때문에 결과적으로 하나의 작업만이 자원에 접근하게 되는 결과를 얻을 수 있다.

NSLock

NSLock은 lock() 메서드를 통해 이미 해당 메서드가 호출되고, unlock() 메서드가 호출되기 전까지 다른 Thread는 기다리게 되면서, 한 자원에 하나의 Thread가 접근할 수 있게 해주는 객체이다.

때문에, 자연스럽게 작업의 줄세우기가 가능해지고, Thread Safe 해진다.

비교적 DispatchSemaphore와 사용법이 비슷하다.

DispatchQueue.global().async {
  for _ in 1...2 {
    lock.lock()
    let book = books.removeFirst()
    lock.unlock()
    print("손님1: \(book)을 샀습니다.")
  }
}

위 코드와 같이 공유 자원에 접근하기전 lock() 메서드를 접근이 끝나고, unlock() 메서드를 호출해주면 된다.

⚠️
NSLock을 사용할 때 주의해야 할 점은 unlock() 메서드를 호출하지 않으면, deaklock 발생되면서, Thread가 멈춰버리게 된다.

Dispatch Barrier

Barrier는 concurrentQueue의 하나의 기능이다.

Barrier는 말 그대로 장벽!! 해당 Queue를 통해 작업이 실행되면, 다음 작업은 Barrier에 걸려 작업이 실행되는 것이 아닌 현재 실행 중인 작업이 끝날 때까지 예약에 걸리게 된다. 때문에, concurrentQueue이지만 흡사, serialQueue와 같이 실행되는 것을 알 수 있다.

손님1: 어린왕자을 샀습니다.
손님1: 백설공주을 샀습니다.
손님2: 해리포터을 샀습니다.
손님2: 신데렐라을 샀습니다.
손님3: 알라딘을 샀습니다.
손님3: 라푼젤을 샀습니다.

위는 concurrentQueue의 Barrier를 이용해 실행한 결과

Barrier를 사용하기 위해선 concurrentQueue가 필요하다.

var barrierQueue = DispatchQueue(
    label: "barrierQueue",
    attributes: .concurrent
)

그리고 해당 Queue를 이용해

barrierQueue.async(flags: .barrier) {
  for _ in 1...2 {
    let book = books.removeFirst()
    print("손님1: \(book)을 샀습니다.")
  }
}
barrierQueue.async(flags: .barrier) {
  for _ in 1...2 {
    let book = books.removeFirst()
    print("손님2: \(book)을 샀습니다.")
  }
}

asyncflags 파라미터를 .barrier로 설정해주면, 클로져 내부의 코드는 순차적으로 실행되게 된다.

'Swift' 카테고리의 다른 글

Combine 뽀개기 1장: Publisher  (0) 2023.07.26
[Swift] Swift Concurrency와 GCD  (0) 2022.12.14
[Swift] Protocol  (0) 2022.02.23
[Swift] 객체지향(OOP)  (0) 2022.02.23
[Swift] 옵셔널 추출  (1) 2022.02.14
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
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 31
글 보관함