concurrency problem(동시성 문제)

멀티 쓰레딩 환경에서 공유자원에 동시에 접근하여 발생하는 문제를 말한다.

 

동시성 문제에는 크게 3가지가 있다. 

  • Race Condition (경쟁 상태)
  • DeadLock (교착 상태)
  • Priority Inversion (우선순위 뒤바뀜)

 

 

Race Condition 

Race Condition(경쟁상태)이란,

공유자원에 여러 쓰레드가 동시에 접근하게 되어, 작업의 접근 순서에 따라 결과가 바뀌는 문제를 의미한다. 

더 정확히 얘기하자면, 공유자원에 대한 쓰기 작업에 의해 발생한다. (읽기 작업은 상관없음)

for i in 1...5 {
    print("--- \(i)번째 ---")
    
    let group = DispatchGroup()
    
    var someNumber = 10
    

    DispatchQueue.global().async(group: group) {
        sleep(1)
        someNumber *= 10
        print("after multiple 10 : \(someNumber)")
    }
    DispatchQueue.global().async(group: group) {
        sleep(1)

        someNumber += 1
        print("after add 1 : \(someNumber)")
    }
    
    group.notify(queue: .main) {
        print("\(i)번째 결과 : \(someNumber)")
    }
    
    group.wait()
}

global default Queue는 Concurrency Queue로, 실제 여러 Thread와 연결되어 있다. 

 

따라서, 여러 Thread에서 someNumber이라는 공유자원에 접근하게 되면서

동시성 문제 중 경쟁 상태 문제가 발생한다.

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

실제 결과를 살펴보면, 

매 시행마다 작업의 접근 순서에 따라 최종 결과가 다르게 나타나며 

10을 곱하거나 1을 더한 후의 결과가 원하지 않는 결과가 출력된다. 

(예를 들어, 1번째 의 경우에는 10을 곱한 후 100이 출력되어야 하는데, 1까지 더해진 101이 출력된다.)

 

위의 코드를 그림으로 표현하자면,

someNumber에 10을 곱하고, print하는 task와

someNumber에 1을 더하고, print하는 task가 Queue에 담긴다.

 

Thread에 배치가 되고, someNumber에 대한 연산을 진행하기 위해 

메모리로부터 someNumber에 대한 읽기 작업을 진행한다.

각 Task를 세분화하면 다음과 같다.

즉, 더하기 Task의 쓰기 작업이 곱하는 Task의 print작업보다 빨리 끝나게 된다면, 

곱하는 Task의 print는 덧셈 진행 후의 결과를 출력하게 된다. 

 

Lazy var의 초기화

lazy var는 해당 프로퍼티가 처음 사용되는 시점에 초기화가 진행되는데, 

thread-safe 하지 않기 때문에, 여러 Thread에서 접근할 경우 여러 번 초기화되는 현상이 발생한다.

class LazyVarClass {
    lazy var someNumber = Int.random(in: 0...1000)
}

 
let someInstance = LazyVarClass()

for _ in 1...10 {
    DispatchQueue.global().async {
        print(someInstance.someNumber)
    }
}

/* prints:
 762
 311
 941
 396
 413
 843
 212
 235
 313
 608
*/

 

 

Race Condition은 Thread-Safe하게 구현함으로써 해결할 수 있는데 

이에 대해서는 다음 포스팅에서 다룰 것이다.

 

 

DeadLock

DeadLock(교착 상태)란, 

두 개 이상의 작업이 서로의 작업이 끝나기만을 기다리게 되면서 결과적으로는 아무것도 못하는 상태를 말한다.

이에 대해서는 GCD 포스팅에서 다루었기 때문에, 자세한 내용은 해당 포스팅을 참고 바란다. 

DispatchQueue.global().async {
    
    DispatchQueue.global().sync {
        task()
    }
}

위의 코드에서는 Default Global Queue에 의해 같은 Thread에 배정된 경우, 

DeadLock이 발생한다. 

 

DeadLock의 경우에는 단순하게 Serial Queue로 작업을 순차적으로 실행함으로써 해결이 가능하다.

다만, 위의 코드는 같은 Thread로 sync로 작업을 보내는 코드상의 오류이기 때문에,

Serial Queue가 아닌 설계 시에 유의해야 한다.

 

추가적으로, DispatchSemephore에서도 잘못된 순서 설계로 인한 DeadLock이 발생할 수 있으니, 유의해야 한다.

 

 

Priority Inversion

Qos를 통해 작업 혹은 Queue에 우선순위를 지정해 줌으로써, 

높은 우선순위를 가진 작업 혹은 Queue는 더 많은 resource를 사용해 빠르게 처리된다.

 

Priority Inversion이란, Qos가 낮은 작업이 Qos가 높은 작업보다 먼저 처리되는 현상을 의미한다.

let highQos = DispatchQueue.global(qos: .userInteractive)
let middleQos = DispatchQueue.global(qos: .utility)
let lowQos = DispatchQueue.global(qos: .background)

let semaphore = DispatchSemaphore(value: 1)


highQos.async {
    sleep(2)
    semaphore.wait()
    print("높은 우선순위 작업 진행")
    semaphore.signal()
}

for i in 1...5 {
    middleQos.async {
        print("중간 우선순위 \(i)번째 작업 진행")
    }
}

lowQos.async {
    semaphore.wait()
    print("낮은 우선순위 작업 진행")
    semaphore.signal()
}

위의 코드의 결과는 예상과는 다르게 아래와 같다.

/* prints:
 중간 우선순위 1번째 작업 진행
 중간 우선순위 3번째 작업 진행
 중간 우선순위 2번째 작업 진행
 중간 우선순위 4번째 작업 진행
 중간 우선순위 5번째 작업 진행
 낮은 우선순위 작업 진행
 높은 우선순위 작업 진행
*/

 

해당 코드를 그림으로 살펴보면 다음과 같다.

이렇게 Queue에 작업들이 담기게 되고, 우선순위가 가장 높은 Task부터 Thread에 배치되어 실행한다 가정하자.

이때, High Task에서는 sleep(3)에 의해 semaphore작업을 배정받지 못하게 되고,

semaphore의 영향을 받지 않는 middle task가 다음으로 수행된다.

 

그다음 우선순위를 가진 Low Task가 실행이 되고, Semaphore의 자원을 점유하기 때문에

Low Task실행이 완료된 후 High Task가 재게 된다.

 

Priority Inversion의 경우에는 GCD에서 자원을 점유하고 있는 작업의 우선순위를 높임으로써, 알아서 해결한다. 

해당 방법 외에도, 공유자원에 접근할 때는 동일한 Qos를 사용함으로써 해결할 수 도 있다.

 

 

Reference

위키피디아

인프런 "앨런"님 강의

복사했습니다!