지난 포스팅을 통해 Race Condition, DeadLock, Priority Inversion과 같은 Concurrency Problem들에 대해 알아보았다. 

Race Condition은 Thread-Safe하게 코드를 작성함으로써 해결할 수 있다고 언급하였는데, 우선 Thread-Safe가 무엇인지에 대해 알아보자. 

 

Thread-Safe멀티 쓰레딩 환경에서 어떤 함수, 변수 혹은 객체

여러 Thread로부터 동시에 접근하여도 프로그램의 실행에 문제가 없음을 의미한다. 

엄밀히 말하자면, 동시에 여러 Thread에서 접근하여도 결과에 이상이 없음을 의미한다. 

 

이러한 Thread-Safe를 지키기 위해 여러 가지 방법이 존재하는데, 이 중 Mutual Exclusion(상호 배제)을 알아보자. 

 

Mutual Exclusion(상호 배제)란,

다수의 Thread에서 공유자원에 접근할 때, 하나의 Thread만 접근 가능하도록 하는 것을 의미한다.

 

 

Readers-Writers Problem

Readers-Writers Problem이란, Readers와 Writers들이 공유자원에 접근할 때 발생하는 문제이다.

여러명의 Readers는 공유자원에 접근하여 데이터를 읽어올 수 있지만

Writer가 접근하여 데이터에 대한 쓰기 작업을 진행할 때는 작업을 진행중인 Writer만 접근이 가능하며,

다른 Readers와 Writers들은 접근이 불가능하다

 

멀티 쓰레딩 환경에서는

공유자원에 대한 Read작업은 Concurrent하게 진행해도 되지만, 

Write작업을 하는 동안은 해당 Thread를 제외한 다른 Thread는 접근이 불가능하다. 

 

따라서, Thread-Safe한 코드를 위해, 

Write작업에 대해 Mutual Exclusion(상호 배제)를 필수적으로 적용해주어야 한다. 

 

 

TSan (Thread Sanitizer)

Xcode에서는 Thread-Safe하지 않는 코드를 찾을 수 있는 TSan이라는 툴을 제공한다.

Product → Scheme → Edit Scheme → Run → Diagostics → Thread Sanitizer 체크

Thread Sanitizer를 체크하고 빌드하게 되면, Thread-Safe하지 않는 코드를 체크해 주게 된다.

 

 

DispatchSemaphore

이전 포스팅에서 살펴보았던 DispatchSemaphore역시 Semaphore를 통하여 Mutual Exclusion이 가능하다.

let semaphore = DispatchSemaphore(value: 1)

for i in 1...5 {
    print("--- \(i)번째 ---")
    
    let group = DispatchGroup()
    
    var someNumber = 10
    
    semaphore.wait()
    DispatchQueue.global().async(group: group) {
        sleep(1)
        someNumber *= 10
        print("after multiple 10 : \(someNumber)")
        semaphore.signal()
    }
    
    semaphore.wait()
    DispatchQueue.global().async(group: group) {
        sleep(1)
        someNumber += 1
        print("after add 1 : \(someNumber)")
        semaphore.signal()
    }
    
    group.notify(queue: .main) {
        print("\(i)번째 결과 : \(someNumber)")
    }
    
    group.wait()
}
/* prints:
 --- 1번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 2번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 3번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 4번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 5번째 ---
 after multiple 10 : 100
 after add 1 : 101
 1번째 결과 : 101
 2번째 결과 : 101
 3번째 결과 : 101
 4번째 결과 : 101
 5번째 결과 : 101
*/

 

 

SerialQueue + sync

이는 매우 엄격한 thread-safe로써, 

해당 SerialQueue가 공유 자원에 접근하는 유일한 방법이라면, 

sync메서드는 Mutual Exclusion Lock(상호 배제 락)처럼 

Main Thread를 제외한 모든 Thread에서 일관된 값을 얻는 것을 보장한다.

let serialQueue = DispatchQueue(label: "jung")

for i in 1...5 {
    print("--- \(i)번째 ---")
    
    let group = DispatchGroup()
    
    var someNumber = 10
    
    DispatchQueue.global().async(group: group) {
        
        serialQueue.sync {
            sleep(1)
            someNumber *= 10
            print("after multiple 10 : \(someNumber)")
        }
    }
    
    DispatchQueue.global().async(group: group) {
        
        serialQueue.sync {
            sleep(1)
            someNumber += 1
            print("after add 1 : \(someNumber)")
        }
    }
    
    group.notify(queue: .main) {
        print("\(i)번째 결과 : \(someNumber)")
    }
    
    group.wait()
}

이들의 결과를 살펴보게 되면, 아래와 같다.

/* prints:
 --- 1번째 ---
 after add 1 : 11
 after multiple 10 : 110
 --- 2번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 3번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 4번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 5번째 ---
 after multiple 10 : 100
 after add 1 : 101
 1번째 결과 : 110
 2번째 결과 : 101
 3번째 결과 : 101
 4번째 결과 : 101
 5번째 결과 : 101
*/

해당 결과가 Race Condition같이 출력이 되었지만, 결론부터 얘기하자면 Race Condition이 아니다. 

Race Condition은 공유자원에 여러 Thread에서 접근하였을 때, 접근 순서에 따라 결과가 바뀌는 상태를 의미한다.

Race Condition의 정의를 다시 살펴보면 "공유자원에 여러 Thread에서 접근하였을 때" 란 조건이 있지만, 

해당 코드에선 공유자원에 하나의 Thread만 접근하였으므로, Mutual Exclusion을 통해 Thread-Safe를 만족한다. 

따라서, 해당 코드는 Race Condition이 아니다. 

 

 

GCD Barrier

barrier는 위의 방법처럼 엄격한 Thread-Safe는 아니지만, 효율적인 방법이다. 

barrier는 DispatchWorkItemFlags타입의 옵션중 하나이다.

public func async(group: DispatchGroup? = nil,
                  qos: DispatchQoS = .unspecified,
                  flags: DispatchWorkItemFlags = [],
                  execute work: @escaping @convention(block) () -> Void)

 

Concurrent Queue내에서 barrier옵션을 적용한 작업이 진행될 때는 Serial 하게 진행된다. 

 

for i in 1...5 {
    print("--- \(i)번째 ---")
    
    var someNumber = 10
    
    let concurrentQueue = DispatchQueue(label: "jung", attributes: .concurrent)
    
    concurrentQueue.async(flags: .barrier) {
        sleep(1)
        someNumber *= 10
        print("after multiple 10 : \(someNumber)")
    }
    
    concurrentQueue.async(flags: .barrier) {
        sleep(1)
        someNumber += 1
        print("after add 1 : \(someNumber)")
    }
    sleep(3)
}

/* prints:
 --- 1번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 2번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 3번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 4번째 ---
 after multiple 10 : 100
 after add 1 : 101
 --- 5번째 ---
 after multiple 10 : 100
 after add 1 : 101
*/

추가적으로 DispatchWorkItem에서도 해당 옵션을 선택해 줄 수 있다.

 

위의 방법들을 종합하여, 객체의 프로퍼티들에 대해 아래와 같이 접근이 가능하다.

let concurrentQueue = DispatchQueue(label: "jung", attributes: .concurrent)

var number: Int

var someNumber: Int {
    get {
        concurrentQueue.sync {
            return self.number
        }
    }
    set {
        concurrentQueue.async(flags: .barrier) {
            self.someNumber = newValue
        }
    }
}

 

 

Reference

위키피디아

Apple Documentation

인프런 "앨런"님 강의

Concurrency in Swift: Reader Writer Lock

 

복사했습니다!