[UIKit] Concurrency: Actor

Junyoung Park·2022년 12월 27일
0

UIKit

목록 보기
139/142
post-thumbnail
post-custom-banner

Concurrency using Actors | Swift 5.5 | Async/Await | Data Race

Concurrency: Actor

Race Problem

  • 두 개 이상의 비동기 태스크가 여러 스레드에서 실행될 경우 data inconsistency 발생할 수 있음.
  • 레이스 문제를 해결하기 위한 방법 중 스레드 세이프를 보장하는 방법이 존재
class Flight {
    let company = "Luft Hansa"
    var availableSeats = ["1A", "1B", "1C"]
    func getAvailableSeats() -> [String] {
        availableSeats
    }
    func bookSeat() -> String {
        return availableSeats.removeFirst()
    }
}
  • 한정된 데이터가 존재하고 특정 함수를 통해 해당 데이터를 사용, 변경되는 경우
private func testFlightProblem1() {
        let flight = Flight()
        let queue1 = DispatchQueue(label: "queue1")
        let queue2 = DispatchQueue(label: "queue2")
        
        queue1.async {
            let bookedSeat = flight.bookSeat()
            print("Booked seat is \(bookedSeat)")
        }
        queue2.async {
            let availableSeats = flight.getAvailableSeats()
            print("Available Seats \(availableSeats)")
        }
    }
  • 두 개 이상의 서로 다른 스레드에서 해당 함수를 사용해 동일한 데이터에 접근할 경우 레이스 문제 발생 가능
Booked seat is 1A
Available Seats ["1A", "1B", "1C"]
  • 첫 번째 큐에서 작성한 태스크와 두 번째 큐에서 작성한 태스크가 동일한 데이터를 건드리고 있지만 비동기적으로 작동하기 때문에 태스크가 끝나고 나서 시작한 지 보장할 수 없음

DispatchQueue: Barrier Flag

  • 배리어 플래그를 통해 이후 태스크를 실행할 스레드의 실행을 '블럭'시킬 수 있음
class Flight {
    let company = "Luft Hansa"
    var availableSeats = ["1A", "1B", "1C"]
    let barrierQueue: DispatchQueue = DispatchQueue(label: "barrierQueue", attributes: .concurrent)
    func getAvailableSeats() -> [String] {
        barrierQueue.sync(flags: .barrier) {
            return availableSeats
        }
    }
    func bookSeat() -> String {
        barrierQueue.sync(flags: .barrier) {
            return availableSeats.removeFirst()
        }
    }
}
  • 컨커런트한 배리어 큐를 별도로 실행한 뒤, 레이스 문제가 발생할 수 있는 구문을 해당 큐의 배리어 플래그가 추가된 상태에서 실행
Booked seat is 1A
Available Seats ["1B", "1C"]

Actor

  • 참조 타입, 프로토콜, 제네릭 등을 사용 가능하다는 점에서 클래스와 매우 유사. 상속 또한 사용 가능. 하지만 data inconsistency와 같은 동시성 문제를 신경쓸 필요가 없음
  • 액터 내부 데이터: let, var과 같은 데이터의 가변성에도 영향을 받음 - 상수라면 문제 없이 참조 가능하지만, 값이 바뀔 수 있는 변수라면 별도의 블럭을 추가해야 함
  • 저장 프로퍼티가 아니라 연산 프로퍼티라면 동일한 문제 발생 (var임)
  • nonisolated라는 키워드를 통해 컴파일러에게 해당 데이터는 var임에도 불구하고 let과 같이 레이스 문제가 일어나지 않는다고 강제할 수 있음
  • async await와 함께 액터 값을 조회/참조/변경 가능
  • Task 블럭 내부 비동기 구문 작성
actor Flight {
    let company = "Luft Hansa"
    var availableSeats = ["1A", "1B", "1C"]
    func getAvailableSeats() -> [String] {
        return availableSeats
    }
    func bookSeat() -> String {
        return availableSeats.removeFirst()
    }
}
  • 액터로 선언한 데이터
private func testFlightProblem2() {
        let flight = Flight()
        let queue1 = DispatchQueue(label: "queue1")
        let queue2 = DispatchQueue(label: "queue2")
        
        queue1.async {            
            Task {
                let bookedSeat = await flight.bookSeat()
                print("Booked seat is \(bookedSeat)")
            }
        }
        queue2.async {
            Task {
                let availableSeats = await flight.getAvailableSeats()
                print("Available Seats \(availableSeats)")
            }
        }
    }
  • Task, async await를 통해 비동기 구문 블럭 사용
Available Seats ["1A", "1B", "1C"]
Booked seat is 1A
  • 위 상황에서는 queue2의 블럭이 먼저 실행되었음

Main Actor

private func testFlightProblem3() {
        let flight = Flight()
        let queue1 = DispatchQueue(label: "queue1")
        let queue2 = DispatchQueue(label: "queue2")
        
        queue1.async {            
            Task { [weak self] in
                let bookedSeat = await flight.bookSeat()
                print("Booked seat is \(bookedSeat)")
                self?.updateLabel(seat: bookedSeat)
            }
        }
        queue2.async {
            Task {
                let availableSeats = await flight.getAvailableSeats()
                print("Available Seats \(availableSeats)")
            }
        }
    }
  • Task의 비동기 구문은 현재 메인 스레드에서 실행되고 있지 않지만, 조회한 데이터를 UI로 갱신해야 하는 상황
@MainActor
    private func updateLabel(seat: String) {
        queueOneLable.text = seat
    }
  • 백그라운드 스레드에서 실행된 함수라 할지라도 @MainActor을 따르는 함수이기 때문에 해당 태스크는 디스패치 메인 큐에서 실행, 즉 UIKit과 연결된 메인 스레드에서 실행되는 게 확실하게 보장
profile
JUST DO IT
post-custom-banner

0개의 댓글