[Swift] Race Condition과 Thread-safe

OQ·2022년 3월 24일
1

iOS

목록 보기
8/8
post-thumbnail

Race Condition 이란?
두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓸 때, 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 그 실행 결과가 달라지는 상황

코드로 설명하는게 개발자이니 바로 본론으로 들어가겠습니다.
다음 코드는 User의 token 값을 갱신한 후 사용한다는 가정의 코드입니다.
OAuth Token을 갱신/인증 한다는 계념으로 생각하시면 될 듯 합니다.

class User {
    var token = 0	// 인증 토큰

    func renewToken() {	// 인증 토큰을 서버에서 갱신한다는 가정
        print("renew token start...")
        sleep(1)  // 통신 딜레이
        user.token += 1
        print("renew token finish - \(user.token)")
    }
}

let queue01 = DispatchQueue(label: "queue01", attributes: .concurrent)
let queue02 = DispatchQueue(label: "queue02", attributes: .concurrent)
var user = User()

// 1초마다 인증 토큰 갱신
queue01.async {
    (1...4).forEach { _ in
        user.renewToken()
        sleep(1)
    }
}

// 1초마다 인증 토큰 사용
queue01.async {
    (0...5).forEach { _ in
        sleep(1)
        print("[queue01] get - \(user.token)")
    }
}

// 1초마다 인증 토큰 사용
queue02.async {
    (0...5).forEach { _ in
        sleep(1)
        print("[queue02] get - \(user.token)")
    }
}

// 2초마다 인증 토큰 사용
queue02.async {
    (0...3).forEach { i in
        sleep(2)
        print("[queue02] get - \(user.token)")
    }
}

인증 토큰은 1초마다 갱신되고 있고 여러 Thread에서 토큰을 계속 사용하고 있는 모습입니다.
우리의 생각과는 달리 결과는 다음과 같습니다.

renew token start...
[queue02] get - 0
renew token finish - 1
[queue01] get - 0
[queue02] get - 1
[queue02] get - 1
[queue01] get - 1
renew token start...
renew token finish - 2
[queue01] get - 2
[queue02] get - 2
[queue02] get - 2
[queue01] get - 2
[queue02] get - 2
renew token start...
[queue01] get - 3
renew token finish - 3
[queue02] get - 3
[queue02] get - 3
renew token start...
[queue02] get - 3
[queue01] get - 3
renew token finish - 4
[queue02] get - 4

(문제의 부분을 볼드체 표시하였습니다.)

renew token finish 이후에 갱신된 값을 사용해야하는데 그렇지 못한 부분이 있고
renew token finish로 갱신하기도 전에 미리 갱신된 값을 가져와서 사용하는 부분도 있습니다.
이런 경우가 실제로 있었다면 전부 인증 실패로 나왔을 겁니다. 😱😱😱

전형적인 Race Condition 상황입니다.
여러 Thread 들이 잠시도 기다리지 못한고 값에 접근하는 통에 일어난 현상입니다.

이제 해결 방법을 알아봅시다.

다음은 User 값 설정을 Thread-safe하게 변경한 코드입니다.

class User {
    private var queue = DispatchQueue(label: "lock.queue", attributes: .concurrent)
    private var _token = 0

    var token: Int {
        get {
            return queue.sync {
                return _token
            }
        }
    }

    func renewToken() {	// 인증 토큰을 서버에서 갱신한다는 가정
        queue.async(flags: .barrier) {
            print("renew token start...")
            sleep(1)  // 통신 딜레이
            self._token += 1
            print("renew token finish - \(self._token)")
        }
    }
}

설명하자면 token에 대한 접근은 "lock.queue"라는 task에서만 접근할 수 있게 하였고 barrier 를 이용하여 Thread-safe하지 않은 접근은 블락시키도록 하였습니다.

다음은 Thread-safe하게 바꾼 후의 결과

renew token start...
renew token finish - 1
[queue02] get - 1
[queue01] get - 1
renew token start...
renew token finish - 2
[queue01] get - 2
[queue02] get - 2
[queue02] get - 2
renew token start...
renew token finish - 3
[queue01] get - 3
[queue02] get - 3
renew token start...
renew token finish - 4
[queue02] get - 4
[queue01] get - 4
[queue02] get - 4

여러 Thread가 싸우지 않고 renew token finish 후 바뀐값을 알맞게 사용해주는 모습입니다.

혹시 여러분들 OAuth 토큰이 가끔 인증 실패가 난다면 Race Condition을 의심해보시는걸 추천드립니다! 😎

profile
덕업일치 iOS 개발자

0개의 댓글