Swift Concurrency 8편 - SwiftConcurrency Data Race 1부

김재형·2024년 6월 30일
0
post-custom-banner

들어가기에 앞서....

Swift Concurrency 1편부터 이어지는 내용임으로 1편부터 보시고 와주시길 바랍니다
시작하겠습니다.

Data Race 란 무엇인가?

데이터 레이스... 직역을 해볼까요? 데이터 경쟁 ?
맞습니다. 데이터 레이스는 이름 그대로 데이터 경쟁을 의미합니다.
좀더 파고들어 보자면.
멀티 쓰레드 / 프로세스 환경 에서 발생하는 현상이며,
여러 쓰레드 / 프로세스동일한 메모리 에 접근하여 할때 발생하는 문제 라고 할수 있죠.

  • 두개 이상의 쓰레드가 동시에 동일 메모리에 접근할때.
  • 접근 할때 하나 이상은 쓰기 작업일때.
  • 쓰레드 간의 접근이 공기화 되지 않았을때.

간단한 예시를 만들어 생각해 보도록 합시다.

import Foundation

class Counter {
    var count = 0 // 초기값을 0으로
    
    func increment() {
        count += 1 값을 1씩 증가 시킵니다.
    }
}

let counter = Counter()
let queue = DispatchQueue.global()

// 10개 비동기 작업을 생성 하겠습니다.
for _ in 0..<10 {
    queue.async {
        for _ in 0..<1000 {
            counter.increment()
        }
    }
}

// 1초 대기하여 모든 작업이 마치도록 합니다.
sleep(1)

print("Final count: \(counter.count)")

자 이때, 여러 스레드가 counter.increment() 메서드를 동시에 호출하게 될것입니다.
여러쓰레드가 count에 접근하게 될것이죠. 결과는 즉 예측할수 없게 됩니다.

각 시도 출력값: Final count: 9905, Final count: 9903, Final count: 9914
즉 같은 값을 공유하게된 상황에서 발생.

GCD 에선 어떻게 해결했을까?

여러가지 방법이 있겠지만 DispatchQueue 를 통해 해결해 보도록 하겠습니다.
label 즉 데이터를 직렬화 해보죠

import Foundation

class Counter {
    private let queue = DispatchQueue(label: "counterQueue")
    private var count = 0
    
    var counter: Int {
        return queue.sync { count }
    }
    
    func increment() {
        queue.async {
            self.count += 1
        }
    }
}

let counter = Counter()
let globalQueue = DispatchQueue.global()

for _ in 0..<10 {
    globalQueue.async {
        for _ in 0..<1000 {
            counter.increment()
        }
    }
}

sleep(1)

print("Final count: \(counter.counter)")

출력값은 Final count: 10000 이 고정입니다.

NSLock 으로도 해결은 합니다.

import Foundation

class Counter {
    private var count = 0
    private let lock = NSLock()
    
    func increment() {
        lock.lock()
        count += 1
        lock.unlock()
    }
    
    var value: Int {
        lock.lock()
        let result = count
        lock.unlock()
        return result
    }
}

let counter = Counter()
let queue = DispatchQueue.global()

for _ in 0..<10 {
    queue.async {
        for _ in 0..<1000 {
            counter.increment()
        }
    }
}

sleep(1)

print("Final count: \(counter.value)")

왜 이 같이 해결이 가능했을까요?

왜 해결이 되었을까?

DispatchQueue 를 통해 해결을 하였을땐, DispatchQueue(label: "counterQueue") 를 생성하여 클래스 내부 변수인 count 에 대한 접근을 관리하게 하였습니다.

이는 즉, 결과값은 하나의 직렬 큐 (serial Queue) 로 생성되어, 큐에 추가된 작업들이 실행 되도록 하였죠.
또한 return queue.sync { count } } 를 통해 동기적으로 값을 반환하게 하였음으로, 해당 작업도 직렬화 되게 합니다.

func increment() 작업에서도 queue.async 를 통해 비동기 적으로 coute 를 증가시키지만
해당 작업은 queue에 비동기적으로 추가 됨으로, 작업이 순서대로 실행 됨으로 쓰레드가 동시에 호출 하더라도, count에 대한 접근은 직렬화 되게 됩니다.

Swift Concurrency 에선?

import Foundation

class HowAboutSwiftConcurrency {
    /// 라면봉지 5개가 있다고 가정해 보죠.
    var ramen = 5

    func eat() {
        ramen -= 1
    }
}

let example = HowAboutSwiftConcurrency()

Task {
    for _ in 0..<5 {
        example.eat()
    }
}

Task {
    for _ in 0..<5 {
        example.eat()
    }
}

Task {
    print("현재 라면은...? : \(example.ramen)")
}

위의 코드에서 Data Race가 발생할 수 있습니다.
여러 Task가 동시에 example.eat() 메서드를 호출하여 ramen 변수를 감소시키고,
동시에 example.ramen 값을 읽습니다.
아까 말했던 DateRace 가 발생할수 있는 원인 3가지 조건이 기억나시나요?
다시 정리해 보죠.

Data Race의 조건

  • 동시 접근 : 여러 Task가 동시에 ramen 변수에 접근.
  • 쓰기 작업 포함 : eat 메서드는 ramen 변수를 수정.
  • 동기화 여부X : 변수 접근이 동기화 되어 있지 않습니다.

Swift Concurrency 에선 어떻게 해결해야 하나?

Actor 의 등장...

Actor

Actor 는 공유되는 가변 상태에 대한 동기화 메커니즘 입니다.
Actor 는 내부 자체에 상태를 가지고 프로그램의 나머지 부분과 격리 되는 특성이 있습니다.
Actor 에 접근 하는 방법이 Actor를 통한 방법이기에 어떤 쓰레드가 Actor 에 접근하려
한다면 다른 코드들은 Actor에 접근하지 못하도록 합니다. 다시 정리해서

Actor의 주요 특징

  • 내부 상태 보호: actor는 내부 상태에 대한 접근을 직렬화하여 Data Race를 방지합니다.
  • 동시성 제어: actor 내부의 모든 메서드 호출은 직렬화되므로, 동시에 여러 스레드가 접근해도 안전합니다.
  • 비동기 메서드: actor 내의 메서드는 기본적으로 비동기입니다. 이는 await 키워드를 사용하여 호출됩니다.

    Actor 는 프로토콜을 채택하고 Extension 구현이 가능합니다.
    또한, 클래스와 마찬가지로, 참조타입이며,
    프로그램에서 인스턴스 데이터 를 나머지와 분리하고 데이터가 동기화 접근을 보장합니다.

import Foundation

actor HowAboutSwiftConcurrency {
    /// 라면봉지 5개가 있다고 가정해 보죠.
    var ramen = 5

    func eat() {
        ramen -= 1
    }
    
    func getRamenCount() -> Int {
        return ramen
    }
}

let example = HowAboutSwiftConcurrency()

Task {
    for _ in 0..<5 {
        await example.eat()
    }
}

Task {
    for _ in 0..<5 {
        await example.eat()
    }
}

Task {
    let ramenCount = await example.getRamenCount()
    print("Remaining ramen: \(ramenCount)")
}

마치며...

요번 시간에는 DataRace 를 다루어 보았는데 아직... 끝이 아닙니다.
더 많은 내용들이 있습니다... 이번 시간도 긴글 읽어 주셔서 감사드리며. 다음 시간엔 Data Race 2부 - 부제목 Actor 를 다루어 보도록 하겠습니다. 감사합니다.

profile
IOS 개발자 새싹이
post-custom-banner

0개의 댓글