[UIKit] Concurrency: Operations & Operation Queue

Junyoung Park·2022년 12월 26일
0

UIKit

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

Mastering Concurrency in iOS - Part 5 (Operations and Operation Queue)

Concurrency: Operations & Operation Queue

Operation

  • GCD에 비해 실행 상태, 기능을 조정해야 할 때 사용
  • 서로 다른 태스크 간의 의존성, 재사용되는 함수 블럭의 캡슐화 등을 고려해야 할 때
  • GCD의 탑 레벨에서 오퍼레이션은 일종의 최고로 추상화된 레이어
  • 단일한 태스크와 연관된 코드 및 데이터를 표현하는 추상화된 단계
  • block, invocation 오퍼레이션이 존재하지만 스위프트에서 지원하는 오퍼레이션은 전자. 후자는 오브젝트 C에서만 지원
  • isReady
  • isExecuting
  • isCancelled
  • isFinished
  • 하나의 인스턴스는 단 한 번만 실행 가능
private func testOperations1() {
        let operation: BlockOperation = .init {
            print("First Test")
            // as sync
            sleep(3)
        }
        operation.start()
    }
  • 중간에 슬립을 주었기 때문에 블럭 오퍼레이션이 실행되고 3초 이후에 빠져나옴
    private func testOperations2() {
        let operation: BlockOperation = .init()
        operation.addExecutionBlock {
            print("First block executed")
        }
        operation.addExecutionBlock {
            print("Second block executed")
        }
        operation.addExecutionBlock {
            print("Third block executed")
        }
        operation.start()
        /*
         About to begin operation
         First block executed
         Third block executed
         Second block executed
         Operation executed
         */
        // as async
    }
  • 하나의 블럭 오퍼레이션에 익스큐션 블럭을 추가해서 시작
  • 비동기적으로 실행되는 블럭
    private func testOperations3() {
        let operation: BlockOperation = .init()
        operation.completionBlock = {
            print("Execution completed")
        }
        
        operation.addExecutionBlock {
            print("First block executed")
        }
        operation.addExecutionBlock {
            print("Second block executed")
        }
        operation.addExecutionBlock {
            print("Third block executed")
        }
        DispatchQueue.global().async {
            operation.start()
            print("Did this run main thread: \(Thread.isMainThread)")
        }
        /*
         About to begin operation
         Operation executed
         First block executed
         Third block executed
         Second block executed
         Execution completed
         Did this run main thread: false
         */
        // even if in global async, should be serialized following the concept of operation execution blocks.
    }
  • 글로벌 큐에서 오퍼레이션이 비동기적으로 실행될 때, 블럭이 추가된 순서대로의 실행 순서를 보장하고 싶을 때에는 다른 방법을 모색해야 함
    private func testOperationQueues1() {
        let operationQueue: OperationQueue = .init()
        // operationQueue.maxConcurrentOperationCount = 1
        // with this property, serialization can be done
        let operation1: BlockOperation = .init()
        operation1.addExecutionBlock {
            print("Operation 1 being executed")
            for i in 1...10 {
                print(i)
            }
        }
        operation1.completionBlock = {
            print("Operation 1 executed")
            // completion block timing issue
        }
        
        let operation2: BlockOperation = .init()
        operation2.addExecutionBlock {
            print("Operation 2 being executed")
            for i in 11...20 {
                print(i)
            }
        }
        operation2.completionBlock = {
            print("Operation 2 executed")
        }
        
        operation2.addDependency(operation1)
        // operation2 -> operation1. i.e. operation2 should wait for operation1 to be completed
        
        operationQueue.addOperation(operation1)
        operationQueue.addOperation(operation2)
        // by default -> operation queue's task should be done concurrently
    }
  • 하나의 오퍼레이션 큐를 만든 뒤 해당 큐에 여러 개의 오퍼레이션을 넣는 것
  • 디폴트 값으로는 큐에 들어간 오퍼레이션 블럭 실행은 또한 컨커런트하게 실행
  • 오퍼레이션 1의 태스크를 2보다 '먼저' 실행해야 할 때에는 다음과 같은 방법 → 오퍼레이션 1을 먼저 작성했다면, maxConcurrentOperationCount 값을 설정함으로써 컨커런트하게 실행 가능한 오퍼레이션 개수를 제한해버리는 것. → 일반적으로는 오퍼레이션 간의 디펜던시를 통해 실행 순서를 보장
private func printOneToTen() {
        DispatchQueue.global().async {
            for i in 1...10 {
                print(i)
            }
        }
    }
    
    private func printElevenToTwenty() {
        DispatchQueue.global().async {
            for i in 11...20 {
                print(i)
            }
        }
    }
  • 다음과 같은 프린트 함수를 각각 오퍼레이션 1, 2에서 실행한다고 가정해보자.
private func testOperationQueues2() {
        let operationQueue: OperationQueue = .init()
        let operation1: BlockOperation = .init(block: printOneToTen)
        let operation2: BlockOperation = .init(block: printElevenToTwenty)
        operation2.addDependency(operation1)
        operationQueue.addOperation(operation1)
        operationQueue.addOperation(operation2)
    }
  • 디펜던시를 추가한다 할지라도 순차적 태스크 실행이 보장되지 않을 수 있음

    개인적으로 실행했을 때에는 언제나 디펜던시에 따라 태스크가 실행이 되었는데, 강의 영상에서는 프린트되는 숫자를 보면 컨커런트했다...

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
    }
}
  • 커스텀 오퍼레이션 클래스를 구현해 비동기 태스크를 수행
  • KVO 패턴을 따라 willSet, didSet을 통해 해당 값의 변화를 관찰
  • 언제나 비동기적인 클래스
  • start() 함수를 오버라이드해서 현재 실행 중인 상태로 변경한 뒤 메인 함수를 실행
class PrintNumbersOperation: AsyncOperation {
    var range: Range<Int>
    init(range: Range<Int>) {
        self.range = range
    }
    
    override func main() {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            for i in self.range {
                print(i)
            }
            self.state = .isFinished
        }
    }
}
  • 위의 비동기 커스텀 오퍼레이션 클래스를 상속받는 프린트 오퍼레이션의 메인 함수는 글로벌 큐에서 이니셜라이즈 당시 파라미터로 입력받은 정수 구간을 출력한 뒤 종료되는 구조
private func testOperationQueues3() {
        let operationQueue: OperationQueue = .init()
        let operation1: PrintNumbersOperation = .init(range: Range(0...25))
        let operation2: PrintNumbersOperation = .init(range: Range(26...50))
        operation2.addDependency(operation1)
        operationQueue.addOperation(operation1)
        operationQueue.addOperation(operation2)
        // serialized when async, using dependency
    }
  • 커스텀 비동기 오퍼레이션 클래스를 사용한 위의 오퍼레이션 큐는 언제나 디펜던시에 따라서 실행되는 게 보장
profile
JUST DO IT
post-custom-banner

0개의 댓글