Do.

RxSwift - MainScheduler.instance vs MainScheduler.asyncInstance 본문

iOS

RxSwift - MainScheduler.instance vs MainScheduler.asyncInstance

Hey_Hen 2023. 6. 2. 14:18

개요

RxSwift를 사용하게 되면 View Layer에서 사용하게 될 때는 꼭 메인 스레드로 변경하도록 코드를 지정해줘야 합니다.

이때 일반적으로 저희가 사용하는 코드는

MainScheduler.instanceMainScheduler.asyncInstance 가 있는데, 이 둘은 근본적으로 무슨 차이가 있을까요?

우선 RxSwift 문서에 의하면 다음과 같이 설명하고 있습니다.

요약하자면 메인 큐에 작업 스케줄이 이미 된 코드라면 별도의 스케줄링 없이 바로 동작한다고 적혀있습니다.

무슨 뜻인지 좀더 확실하게 알아볼까요?

override func scheduleInternal<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
  let previousNumberEnqueued = increment(self.numberEnqueued)

  if DispatchQueue.isMain && previousNumberEnqueued == 0 {
    let disposable = action(state)
    decrement(self.numberEnqueued)
    return disposable
  }

  let cancel = SingleAssignmentDisposable()

  self.mainQueue.async {
    if !cancel.isDisposed {
      cancel.setDisposable(action(state))
    }

    decrement(self.numberEnqueued)
  }

  return cancel
}

RxSwift에서 스캐줄링을 하게 되면 위 메서드가 실행되는데요, 조건을 보시면 아시겠지만 만약 이미 Main Queue이거나, Rx에서 자체적으로 관리하는 스케줄 number가 0이라면 별도의 스케줄링 없이 동기 코드로 즉시 실행하게 됩니다.

반면 Main Queue가 아니거나 Rx의 작업스케줄이 아직 남아있는 상태라면 main queue에 적재해서 비동기로 실행하게 됩니다.

이걸 Rx에서는 최적화라고 표현하고 있는 것이죠 즉 MainScheduler.instance는 MainScheduler의 싱글톤 인스턴스이고, Upstream의 스케줄에 따라 비동기 일 수도 있고 아닐 수도 있습니다.

MainScheduler.asyncInstance

그러면 MainScheduler.asyncInstance는 무엇일까요? MainScheduler 클래스 안에는 Instance 싱글톤 객체 말고도 asyncInstance도 있습니다.

public static let asyncInstance = SerialDispatchQueueScheduler(serialQueue: DispatchQueue.main)

그리고 asyncInstance는 SerialDisatchQueueScheduler라고 합니다.

다시 MainScheduler.Instance 코드를 유심히 보다보면 override func scheduleInternal 는 이미 override 된 함수라는 것을 알 수 있죠.

또 사실 MainScheduler라는 것은 SerialDispatcherQueueScheduler의 서브클래스라는 것도 알 수 있습니다.

그렇다면 SerialDispatchQueueScheduler의 scheduleInternal 본연의 동작은 어떻게 될까요? 타고타고 들어가면 아래 코드가 나옵니다.

func schedule<StateType>(_ state: StateType, action: @escaping (StateType) -> Disposable) -> Disposable {
  let cancel = SingleAssignmentDisposable()

  self.queue.async {
    if cancel.isDisposed {
      return
    }


    cancel.setDisposable(action(state))
  }

  return cancel
}

무조건 async 로 동작하게 됩니다. 그리고 저 queue는 앞서 asyncInstance를 생성할 때 전달한 DispatchQueue.main이죠, 즉 MainScheduler.asyncInstance는 무조건 비동기로 동작하게 됩니다.

결론은 뭐가 다른데?

넵 사실 큰 차이 없습니다. asyncInstance나 Instance나 동일한 Serial Queue이고 작업이 메인 스레드에서 동작할 수 있도록 보장해 줍니다.

다만 Instance의 경우는 그러할 필요가 없다면 Main Queue로 스케줄링 하지 않고 바로 실행한다는 부분이죠.

그래서 만약 MainScheduler.Instance가 잘 동작한다면 최적화 되고 좋은 동작 일 것입니다.

위 코드는 작업은 스케줄링 했을 때와 그렇지 않을 때 코드가 실행된 속도입니다. async가 훨씬 느리죠? RxSwift는 이 부분을 최적화 했다고 볼 수 있습니다.

But?

하지만 이는 RxSwift의 자체 스케줄러가 잘 동작했을 때 문제입니다. 만약 MainScheduler.Instance가 잘 동작한다면 MainScheduler.asyncInstance는 필요없을 겁니다. 따로 있는 이유가 무엇일까요?

func register(synchronizationErrorMessage: SynchronizationErrorMessages) {
  self.lock.lock(); defer { self.lock.unlock() }
  let pointer = Unmanaged.passUnretained(Thread.current).toOpaque()
  let count = (self.threads[pointer] ?? 0) + 1

  if count > 1 {
    self.synchronizationError(
      "⚠️ Reentrancy anomaly was detected.\n" +
      "  > Debugging: To debug this issue you can set a breakpoint in \(#file):\(#line) and observe the call stack.\n" +
      "  > Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`\n" +
      "    This behavior breaks the grammar because there is overlapping between sequence events.\n" +
      "    Observable sequence is trying to send an event before sending of previous event has finished.\n" +
      "  > Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,\n" +
      "    or that the system is not behaving in the expected way.\n" +
      "  > Remedy: If this is the expected behavior this message can be suppressed by adding `.observe(on:MainScheduler.asyncInstance)`\n" +
      "    or by enqueuing sequence events in some other way.\n"
    )
  }

  self.threads[pointer] = count

  if self.threads.count > 1 {
    self.synchronizationError(
      "⚠️ Synchronization anomaly was detected.\n" +
      "  > Debugging: To debug this issue you can set a breakpoint in \(#file):\(#line) and observe the call stack.\n" +
      "  > Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`\n" +
      "    This behavior breaks the grammar because there is overlapping between sequence events.\n" +
      "    Observable sequence is trying to send an event before sending of previous event has finished.\n" +
      "  > Interpretation: " + synchronizationErrorMessage.rawValue +
      "  > Remedy: If this is the expected behavior this message can be suppressed by adding `.observe(on:MainScheduler.asyncInstance)`\n" +
      "    or by synchronizing sequence events in some other way.\n"
    )
  }
}

당연히 필요하기 때문이죠? 방출 이벤트가 너무 많아 복잡해지면 Rx의 자체적인 스캐줄링이 깨지게 되는데, 이때 asyncInstance를 사용해 달라고 경고문이 발생하게 됩니다.

이러한 문제는 얘기치 못한 결과를 낼 수 있고 이미 QA에서 테스트 중일때는 감지하기가 어렵습니다.

즉 개발자가 사전에 이를 알고 Instance를 선택할지 asyncInstance를 선택할지 판단하기 어렵다는 뜻입니다. 그리고 만약 Upstream의 작업 스케줄링을 잘했다면 대부분은 Main Thread가 아닐 것이기 때문에 Instance 동작을 하더라도 사실 꽤 많은 동작이 DispatchQueue.main.async로 스케줄링 된다는 뜻입니다.

MainScheduler.Instance를 호출했을 때 async 블럭을 타는지 바로 실행하는지 결과 출력

다시말해서 간단한 경우는 MainScheduler.Instance를 사용할 수 있지만 복잡한 경우는 asyncInstance를 채용하는 것이 안전한 것이죠.

결론은

Combine 쓰세요(찡긋)

@mint

Comments