[UIKit] Concurrency: QnA

Junyoung Park·2022년 12월 27일
0

UIKit

목록 보기
138/142
post-thumbnail

Interview Questions on Concurrency, GCD, Operation Queue | Swift (Mastering Concurrency in iOS - 6)

Concurrency: QnA

Sync vs Async

  • 동기적 → 현재 스레드를 블럭: 특정 코드를 실행한다면 현재 스레드 실행 중 코드가 실행 완료될 때까지 블럭
  • 비동기적 → 현재 스레드를 블럭하지 않음: 특정 코드를 실행한다면 별도로 실행 OK

Serial Queue vs Concurrent Queue

  • 시리얼: 한 번에 한 태스크
  • 컨커런트: 한 번에 여러 개의 태스크 → 컨커런트 큐라 할지라도 FIFO라는 자료구조의 특성은 모두 보장

Serial vs Sync & Concurrent vs Asnyc

  • 시리얼/컨커런트 큐 → 현재 디스패치되고 있는 목적 큐에 영향을 미침. 어떻게 실행이 순차적으로 또는 동시적으로 진행될 것인지만
  • 동기/비동기 → 해당 스레드로부터 디스패치되고 있는 현재 스레드에 영향을 미침.
    private func testProblem1() {
        let queue = DispatchQueue(label: "printNumbers")
        var numbers: String = ""
        for i in 50...55 {
            numbers += "\(i) "
        }
        print(numbers)
        queue.async {
            var numbers: String = ""
            for i in 10...15 {
                numbers += "\(i) "
            }
            print(numbers)
        }
        queue.async {
            var numbers: String = ""
            for i in 0...5 {
                numbers += "\(i) "
            }
            print(numbers)
        }
        for i in 30...35 {
            numbers += "\(i) "
        }
        print(numbers)
    }
  • 큐는 컨커런트하지 않으므로 디폴트로 시리얼 큐로 구현
  • 두 개의 비동기 실행 블럭이 존재
50 51 52 53 54 55 
30 31 32 33 34 35 
10 11 12 13 14 15 
0 1 2 3 4 5 
  • 비동기라 할지라도 시리얼 큐이기 때문에 10...15 프린트 블럭이 0...5보다 언제나 더 먼저 실행될 것은 확실하게 보장
  • 30...35 프린트 블럭은 비동기 코드 블럭과 별개로 실행되기 때문에 언제 실행될 것인지 전후 상황을 예측 불가능
    private func testProblem2() {
        let queue = DispatchQueue(label: "printNumbers", attributes: .concurrent)
        var numbers: String = ""
        for i in 50...55 {
            numbers += "\(i) "
        }
        print(numbers)
        queue.async {
            var numbers: String = ""
            for i in 10...15 {
                numbers += "\(i) "
            }
            print(numbers)
        }
        queue.async {
            var numbers: String = ""
            for i in 0...5 {
                numbers += "\(i) "
            }
            print(numbers)
        }
        numbers = ""
        for i in 30...35 {
            numbers += "\(i) "
        }
        print(numbers)
    }
  • 컨커런트한 큐로 구현
  • 시리얼 큐가 아니기 때문에 해당 큐 내부에서 실행된 두 개의 비동기 구문 순서는 보장 불가능
50 51 52 53 54 55 
30 31 32 33 34 35 
0 1 2 3 4 5 
10 11 12 13 14 15
  • 일반 (메인 스레드)에서 실행된 50...55, 30...35 블럭은 순서대로 실행 예측 가능
  • 비동기 구문의 두 가지 블럭은 30...35 구문 이전, 이후에 나올 지 예측 불가능
  • 비동기 큐에 들어간 두 가지 태스크(10...15, 0...5)의 순서 또한 컨커런트한 큐인 까닭에 예측 불가능

QoS - Where to use

  • 단일한 파라미터에 의해 조정 가능한 퀄리티
  • 유저 인터렉티브, 유저 이닛, 유틸리티, 백그라운드 등으로 크게 구별 가능
  • 각각 애니메이션(UI 업데이트에 포함?), 즉각적 결과(문제없는 UX 플로우를 제공하는 데 필요한 데이터?), 오랫 동안 작동하는 태스크(유저가 해당 진행 정도를 알고 있는지?), 유저에게 보이지 않는 태스크(유저가 태스크를 알고 있는지?) 등 특징 존재

Multiple API calls

  • 여러 개의 API 호출을 시도한 뒤 해당 태스트들이 모두 종료되었을 때에만 다음 프로세스로 진행 가능한 방법
  • 디스패치 그룹을 사용: enter / leave 함수 사용
  • notify 함수를 통해 캐치
  • 컴바인 프레임워크의 zip을 통해서도 동일한 동작을 작동할 수 있지만, 해당 방법은 업스트림의 성공 값만을 다운스트림으로 전달하는 까닭에 API 호출 이후 태스크 실패 상황을 핸들링하는 데 어려움

Race condition

  • 디스패치 배리어 플래그
  • 디스패치 세마포어
private func addItems(item: ItemModel) {
        semaphore.wait()
        if walletBalance >= item.price {
            PurchaseManager.shared.buyItem(item: item, balance: walletBalance) { [weak self] success in
                guard let self = self else { return }
                if success {
                    DispatchQueue.main.async {
                        self.walletBalance -= item.price
                        self.cartBalance += item.price
                        self.semaphore.signal()
                    }
                }
            }
        }
    }
  • 세마포어의 카운터 값을 커스텀 가능함으로써 현재 크리티컬 섹션에 들어갈 수 있는 스레드 개수를 조정할 수 있음. 디스패치 배리어는 세마포어보다 세밀한 조정을 하는 데 어려움

GCD vs Operation Queue

  • GCD와 오퍼레이션 큐는 사실상 비교 가능한 대상이 아님
  • GCD는 로우 레벨의 API인 반면 오퍼레이션 큐는 탑 레벨의 추상 클래스
  • GCD는 상황이 복잡하지 않고 실행의 상태를 고려하지 않아도 될 때 주로 사용. 태스크에 대한 컨트롤이 그렇게 필요하지 않을 때.
  • 오퍼레이션 큐는 오퍼레이션 간의 디펜던시를 조절하거나 클래스 상속을 통한 재사용 등 추후 컨트롤을 요할 때 사용.

Cancel the Task in GCD

  • 디스패치 워크 아이템 → GCD를 사용하면서 해당 아이템을 취소 가능
  • 하지만 워크 아이템 취소는 오퍼레이션을 취소하는 데 제공된 변수, 함수 등과는 별도로 다소 제한적

Async Operation

  • 오퍼레이션을 비동기적으로 실행하는 방법
  • 오퍼레이션을 상속하는 새로운 클래스를 작성하기 → start(), cancel(), isAsynchronous 등 프로퍼티를 새롭게 작성한다면 해당 클래스는 비동기적으로 작동하는 오퍼레이션임
class AsyncOperation: Operation {
    enum State: String {
        case isReady
        case isExecuting
        case isFinished
    }
    
    var state: State = .isReady {
        willSet(newValue) {
            willChangeValue(forKey: state.rawValue)
            willChangeValue(forKey: newValue.rawValue)
        }
        didSet {
            didChangeValue(forKey: oldValue.rawValue)
            didChangeValue(forKey: state.rawValue)
        }
    }
    override var isAsynchronous: Bool { true }
    override var isExecuting: Bool { state == .isExecuting }
    override var isFinished: Bool {
        if isCancelled && state != .isExecuting { return true }
        return state == .isFinished
    }
    
    override func start() {
        guard !isCancelled else {
            state = .isFinished
            return
        }
        state = .isExecuting
        main()
    }
    
    override func cancel() {
        state = .isFinished
    }
}

Dependency between tasks

  • 오퍼레이션 간의 디펜던시를 추가하는 방법
  • 오퍼레이션 큐에 넣는 오퍼레이션 간의 실행 완료 → 실행 시작과 같은 순서를 확실하게 보장하는 방법

Thread safe class

  • 디스패치 배리어, 세마포어 사용
  • 크리티컬 섹션을 정의한 뒤 들어올 수 있는 스레드 컨트롤 가능
  • 액터 사용
  • 구조체는 값 타입이기 때문에 스레드 세이프한 반면, 참조 타입인 클래스는 스레드 세이프하지 않을 수 있다는 것 역시 체크

UI Update in background thread

  • 백그라운드 스레드에서의 UI 업데이트의 가능 여부
  • UIKit는 메인 스레드와 연결
  • 뷰 드로우 사이클과 연결된 메인 런 루프와 UI 리프레시 이벤트
  • 그래픽 렌더링 또한 메인 스레드에서 해야 하는 이유 → 백그라운드 스레드에서 비동기적으로 실행될 경우 flickering이 발생할 수도 있기 때문
profile
JUST DO IT

0개의 댓글