본문 바로가기
iOS/Combine

#17 Schedulers

by HaningYa 2020. 11. 3.
728x90
  • 책을 읽어오면서 scheduler 를 파라미터로 사용하는 operator 들을 많이 보았다.
  • 대부분의 경우 간편하게 DispatchQueue.main을 사용했었다. 
  • 개발자로써 DispatchQueue뿐만 아니라 global, concurrent queue 나 serial dispatch queue도 사용해 봤을 것 이다.
  • 이번 장에는 이러한 dispatch queue 에 대해 전반적으로 알아본다.
  • 그런데 왜 Combine 비슷한 새로운 컨셉이 필요할까?
  • 실제 세상으로 나가서 combine schedulers가 어디에 쓰이는지 알아보자
  • 이번 챕터에서는 왜 schedulers 컨셉이 등장했는지 알아본다.
  • 어떻게 Combine 이 asynchronous event 와 action을 쉽게 다룰수 있는지 알아본다.

An introduction to schedulers

Apple 문서에 따르면 scheduler 는 protocol 로써 closure 가 언제(when) 어떻게(how) 실행되는지 정의한다.

정의가 틀린 건 아니지만 이것이 scheduler의 전부는 아니다.

scheduler는 future action을 실행하기 위한 맥락을 제공해서 바로 실행되거나 미래에 실행되게 한다.

이 action 은 protocol 에 정의된 closure 이다. 하지만 이 closure라는 용어는 publisher가 특정 일정에 따라 수행되는 some value를 숨길 수도 있다.

이 정의는 의도적으로 threading과의 연관성을 피한것을 느낄 수 있는데 왜냐하면 구체적인 구현은 scheduler protocol 에서 context 가 실행되는 위치를 정의하는 것이기 때문이다.

어떤 thread 에서 코드가 실행될지에 대한 정확한 세부사항은 어떤 scheduler를 선택하는지에 따라 달려있다.

중요한 컨셉을 기억해라: scheduler 는 thread 와 동일하지 않다.

Button -> Main thread (Observe button press) -> Background scheduler(perform computation) -> Main thread (update UI)

  • 사용자가 버튼을 누르는 action 이 main thread 에서 발생한다.
  • 해당 이벤트는 background scheduler 의 몇가지 process 를 trigger 한다.
  • 최종 데이터는 main thread 의 subscriber 에게 전달되고 app UI를 업데이트 한다.

이것을 보면 scheduler 가 foreground/background 실행과 깊은 관련이 있다는 걸 알 수 있다. 

더구나 어떤 형식으로 구현할 지에 따라 serialized 나 parallelized 로 작업을 처리할 수 도 있다.

결국 scheduler를 이해하려면 어떤 class 가 Scheduler protocol 을 준수하는지 알아봐야 한다.

그전에 먼저 schedulers 에 관련된 중요한 두가지 operator 를 배우고 가겠다.

*다음 섹션에서는 DispatchQueue를 사용할건데 Combine Scheduler protocol 을 준수한다.

Operators for scheduling

combine framework는 scheduler와 작업할 수 있는 두가지 기본적인 operator 를 제공한다.

  • subscribe(on:), subscribe(on:options:)
    - 특정 scheduler 에 subscription을 생성한다.
  • receive(on:), receive(on: option:)
    - 특정 scheduler 에 값을 전달한다.

추가적으로 아래의 operator들은 scheduler 와 scheduler option들을 parameter 로 받는다. 챕터 6 Time Manipulation operators 에서 배운 것들이다.

  • debounce(for:scheduler:options:)
  • delay(for:tolerance:scheduler:options:)
  • measureInterval(using:options:)
  • throttle(for:scheduler:latest:)
  • timeout(_:scheduler:options:customError:)

Introducing subscribe(on:)

publisher 는 subscribe 되기 전까진 동작하지 않는다. 하지만 subscribe 되었을 경우엔 아래의 단계를 따른다.

  1. publisher는 subscriber 를 수신하고 subscription을 만든다.
  2. Subscriber는 subscription을 수신하고 publisher로 부터 값을 요청한다.
  3. publisher는 subscription을 통해 작업을 시작한다.
  4. publisher는 subscription을 통해 값을 방출한다.
  5. operators는 value를 변환한다.
  6. subscriber는 최종값을 수신한다.

1,2,3 단계는 publisher에 subscribe 할 때 있던 thread 에서 발생한다.

그리고 subscribe(on:) operator 를 사용하면 모든 operation들은 우리가 명시한 scheduler 에서 돌아가게 된다.

만약 publisher 가 expensive computation 작업을 background에서 수행하고 싶을 땐 subscribe(on:) 을 사용하면 된다.

예제를 보자

  1. starter playground 에는 ExpensiveComputation이라는 시간이 오래걸리고 완료된 후 string 을 emit하는 publisher를 가지고 있다.
  2. 특정 scheduler에 대해 computation을 trigger 하기 위해 serial queue를 사용할 것 이다. DispatchQueue는 Schduler protocol 을 준수한다.
  3. 현재 실행되는 thread 번호를 가져온다. playground 에서 main thread는 1번이고 default thread 이다. number extension은 Thread.swift 에 정의되있다.

이제 computationPublisher를 subscribe 하고 실행해보면

  • 코드는 main thread에서 실행되며 main thread 에서 computation publisher 를 subscribe 한다.
  • publisher 는 subscriber를 받는다.
  • subscription이 이루어지고 작업이 시작된다.
  • 작업이 완료되면 publisher는 결과를 subscription을 통해 전달하고 끝난다.

모든 작업이 thread 1인 main thread 에서 동작하는 걸 볼 수 있다.

이제 publisher subscription 에 subscribe(on:) operator를 추가하고 실행해본다.

main thread 에서 subscribing 하지만 combine 은 subscription이 효과적으로 될 수 있게 queue에게 위임한다.

queue는 queue가 가진 thread들 중 하나에서 코드를 실행한다. computation이 thread4에서 시작되고 끝이난 다음 결과가 방출된다. 

*DispatchQueue의 동적 쓰레드 관리 떄문에 실행할 때 마다 다른 thread 번호를 볼 수 있다.

하지만 이 subscription에서 화면의 정보를 바꾸고 싶다면 어떻게 해야할까?

DispatchQueue.main.async를 sink 클로저에 넣어서 main thread 에서 UI update 가 일어나게 해줘야 한다.

하지만 Combine에는 더 효율적인 방법이 있다.

Introducing receive(on:)

두번째로 중요한 receive(on:) 은 subscriber에서 값을 전달하기 위해 사용할 scheduler 를 명시할 수 있다.

computation 작업은 background thread 에서 emit 되었지만 값은 main queue에서 받아오는걸 보장할 수 있다.

이렇게 UI를 안전하게 업데이트 할 수 있다.

이 부분까지 DIspatchQueue를 사용한 scheduling operator 의 소개였고 Combine 은 Scheduler protocol 을 구현하기 위해 이것을 확장한다. 

Scheduler implementations 

apple은 Scheduler protocol 에 대해 몇가지 구체적인 구현을 제공한다.

  • ImmediateScheduler: subscribe(on:), receive(on:)으로 execution context를 바꾸지 않는 한 현재 thread에서 바로 실행되는 코드를 위한 간단한 scheduler 이다. 
  • RunLoop : Foundation 의 Thread Object 에 묶여있다.
  • DispatchQueue: serial 또는 concurrent 일 수 있다.
  • OperationQueue : 작업 항목에 대해 단속하는 queue

이번 챕터에서는 위에 요소들에 대해 자세히 알아본다.

ImmediateScheduler 

콤바인이 제공하는 스케쥴러 카테고리에서 가장 간단한 스케쥴러이다.

예제를 위해 먼저 간단한 Timer를 만들어 준다.

그리고 publisherfmf aksemsms closure를 준비한다. 이미 Record.swift에 recordThread(using:)으로 준비되어 있다.

이 operator는 operator가 통과하는 값을 볼때 현재의 쓰레드를 기록하고 publisher source 에서 최송 sink까지 여러번 기록할 수 있다.

  1. recorder 를 받는 publisher를 만든다. recordThread(using:) operator는 현재 쓰레드 정보를 기록한다.
  2. timer가 값을 방출하면 현재 쓰레드를 기록한다.
  3. 방출된 값을 ImmediateScheduler에 바로 넘겨준다.
  4. 현재 어느 쓰레드에 있는지 출력한다.
  5. closure는 무조건 AnyPublisher Type을 리턴해야 한다. (내부 구현의 편의성을 위해)
  6. 다양한 레코드 포인트에서 스레드간에 published 된 값의 마이그레이션을 표시하는 ThreadRecorderView를 준비한다.

 

timer 로 부터 방출된 값을 나타내는데 각 줄에서 값이 통과하는 스레드를 볼 수 있다.

recordThread (using :) 연산자를 추가 할 때마다 줄에 추가 스레드 번호가 기록된다.

이번 예제에서는  current thread 가 main thread와 같은걸 알 수 있는데 왜냐하면 ImmediateScheduler를 사용해서 현재 thread 에서 바로 작업하도록 했기 때문이다.

확실하게 하기위해 첫번째 recordThread 이전에 .receive(on: DispatchQueue.global()) 을 넣어준다.

이 요청은 방출되는 값이 global concurrent queue 에서 사용할 수 있도록 해준다. 그래서 결과가 이전과는 다르게 나온다.

왜 다른지는 뒤에가서 배우겠다.

ImmediateScheduler options

대부분의 operator는 파라미터에 스케줄러를 허용하므로 SchedulerOptions 값을 허용하는 옵션 argument를 찾을 수 있다.

그런데 ImmediateScheduler의 경우이 유형은 Never로 정의되어 있으므로 ImmediateScheduler를 사용할 때는 연산자의 옵션 매개 변수 값을 절대 전달해서는 안된다. 

ImmediateScheduler pitfalls

ImmediateScheduler의 한 가지 구체적인 점은 즉각적이라는 것이다.

지연을 지정해야하는 SchedulerTimeType에는 공개 이니셜 라이저가없고 즉각적인 예약에 의미가 없기 때문에 Scheduler 프로토콜의 schedule (after :) 변형을 사용할 수 없다.

이 장에서 배우게 될 두 번째 유형의 스케줄러 인 RunLoop에도 유사하지만 다른 함정이 존재한다.

RunLoop scheduler

오랜 iOS 및 macOS 개발자는 RunLoop에 익숙하다. DispatchQueue 이전 Main (UI) 스레드를 포함하여 스레드 수준에서 입력 소스를 관리하는 방법이다.

애플리케이션의 기본 스레드에는 여전히 연결된 RunLoop이 있다. 현재 스레드에서 RunLoop.current를 호출하여 모든 Foundation 스레드에 대해 하나의 쓰레드 얻을 수도 있다.

*DispatchQueue 가 나오면서 RunLoop는 상대적으로 덜 중요해졌는데 Timer나 UIKit, AppKit 의 경우 RunLoop에 여전히 의존하고 있다.

RunLoop에 대해서 알아보자

  1. 이전과 비슷하게 receive를 통해 global concurrent queue로 값을 전달한다. (재밌기 때매 그냥)
  2. 그리고 RunLoop.current값을 받아본다.

RunLoop.current 는 호출이 이루어 졌을 때 현재의 스레드와 관련된 RunLoop를 뜻한다. 클로저는 Publisher와 Recorder를 설정하기 위해 메인 쓰레드에서 ThreadRecorderView에 의해 호출된다. 따라서 RunLoop.current는 메인 스레드의 RunLoop인 것을 알수 있다.

요청에 따라 첫 번째 recordThread는 각 값이 global concurrent queue의 쓰레드 중 하나를 통과 한 다음 메인 쓰레드에서 계속된다는 걸 보여준다. (RunLoop 로 다시 receive 했는데 현재 RunLoop은 Main thread 임 그래서 Thread1으로 받아짐)

A little comprehension challenge

만약에 receive(on:) 대신 subscribe(on: DispatchQueue.global())를 사용하면 어떻게 될까? 해보면 모든 record는 thread 1 에서 동작하는 걸 볼 수 있다. publisher는 concurrent queue에 구독한것은 맞지만 Timer는 main RunLoop 에서 값을 방출하고 있다. 결과적으로 이 publisher를 구독하기 위해 어떤 scheduler를 택하던지, 값은 항상 main 쓰레드에서 시작할 것 이다.

scheduling code execution with RunLoop

스케줄러를 사용하면 가능한 한 빨리 또는 미래 날짜 이후에 실행되는 코드를 예약 할 수 있다. ImmediateScheduler는 나중에 실행되는데에는 사용할 수는 없었지만 RunLoop은 지연된 실행을 완벽하게 수행 할 수 있다.

각 스케줄러 구현은 자체 SchedulerTimeType을 정의한다. 이것은 어떤 데이터 타입을 사용하는지 알기 전까지 이해하기 복잡하게 만드는데  RunLoop의 경우 SchedulerTimeType 값은 Date 이다.

몇 초 후에 ThreadRecorderView의 구독을 취소하는 작업을 예약해본다. ThreadRecorder 클래스에는 Publisher에 대한 구독을 중지하는 데 사용할 수있는 optional Cancellable이 있다.

먼저 ThreadRecorder 에 대한  레퍼런스를 저장할 수 있는 변수가 필요하다.

var threadRecorder: ThreadRecorder? = nil

이제 threadRecorder instance를 캡쳐해야 하는데 setupPublisher closure에서 한다.

  • 클로저에 명시적으로 type을 추가하고 threadRecorder 변수를 할당 한 다음 publisher를 반환한다.
  • subscription time 에 recorder 를 capture 하기 위해 operator 를 사용한다.

setupPublisher closure 내에 아무데나 코드를 추가한다.

.handleEvents(receiveSubscription: { _ in threadRecorder = recorder })

이제 몇초 뒤 어떤 작업들을 schedule 할 수 있게됬다. 다음 코드를 추가한다.

이 schedule(after:tolerance:)는 시스템이 선택한 시간에 코드를 정확하게 실행할 수 없는 경우 허용 가능한 drift 함께 제공된 클로저를 실행해야하는시기를 예약 할 수 있다. 실행 전에 4개의 값을 보낼 수 있도록 현재 날짜에 4.5 초를 추가한다.

실행해보면 4개의 값을 업데이트 하고 종료되는 것을 볼 수 있다.

RunLoop options

ImmediateScheduler와 마찬가지로 RunLoop은 SchedulerOptions 매개 변수를 사용하는 호출에 적합한 옵션을 제공하지 않는다.

RunLoop pitfalls

RunLoop의 사용은 메인 쓰레드의 실행 루프와 필요한 경우 제어하는 ​​Foundation 쓰레드에서 사용할 수있는 RunLoop으로 제한되어야 한다. 즉, Thread 객체를 사용하여 직접 시작시킨 모든 것입니다.

피해야 할 특정 함정 중 하나는 DispatchQueue에서 실행되는 코드에서 RunLoop.current를 사용하는 것이다. 이는 DispatchQueue 스레드가 일시적 일 수 있으므로 RunLoop에 의존하는 것이 거의 불가능하기 때문이다.

이제 가장 다양하고 유용한 스케줄러 인 DispatchQueue에 대해 배워본다.

DispatchQueue scheduler

DispatchQueue가 스케줄러 프로토콜을 채택하고 스케줄러를 매개 변수로 사용하는 모든 operator가 사용할 수 있다는 것은 놀라운 일이 아니다.

그러나 먼저, Dispatch Queue에 대한 간단한 복습을 해본다. Dispatch 프레임 워크는 시스템에서 관리하는 Dispatch queue 에 작업을 추가하여 멀티 코어 하드웨어에서 코드를 동시에 실행할 수있는 Foundation의 강력한 구성 요소이다.

DispatchQueue는 serial (기본값) 또는 concurrent 일 수 있다. serial queue은 사용자가 공급하는 모든 작업 항목을 순서대로 실행한다.

concurrent queue는 CPU 사용량을 최대화하기 위해 여러 작업 항목을 병렬로 시작한다. 두 queue 유형 모두 용도가 다르다.

Queues and threads

가장 많이 쓰이는 queue은 DispatchQueue.main이다. 메인 (UI) 스레드에 직접 매핑되며 대기열에서 실행되는 모든 작업은 사용자 인터페이스를 자유롭게 업데이트 할 수 있다.

UI 업데이트는 메인 스레드에서만 허용된다. 다른 모든 queue (serial 또는 concurrent)는 시스템에서 관리하는 스레드 풀에서 코드를 실행한다.


즉, 큐에서 실행되는 코드의 현재 스레드에 대해 어떠한 추측도하지 않아야한다. (context가 계속 바뀔 수 있음)

특히 DispatchQueue가 스레드를 관리하는 방식 때문에 RunLoop.current를 사용하여 작업을 예약해서는 안된다. (Idle 한 thread로 언제든 context를 바꿀 수 있기 때문에)

모든 디스패치 큐는 동일한 스레드 풀을 공유한다.

수행 할 작업이 제공되는 serial queue는 해당 풀에서 사용 가능한 모든 스레드를 사용한다. 결과적으로 동일한 큐에서 두 개의 연속되는 작업 항목이 순차적으로 실행되는 동안 다른 스레드를 사용할 수 있다는 것이다.

이것은 중요한 차이점이다. subscribe (on :), receive (on :) 또는 Scheduler 매개 변수를 사용하는 다른 연산자를 사용할 때 스케줄러를 지원하는 스레드가 매번 동일하다고 가정해서는 안된다. (Idle 한 쓰레드로 바꿔서 실행시킬수 있음. 그래서 receive 같은 걸 써서 main 으로 context 변경)

Using DispatchQueue as scheduler

timer를 사용해 값을 방출하고 schedulers 끼리 migration하는걸 지켜볼껀데 이번엔 timer 를 Dispatch Queue timer를 사용한다.

timer로 부터 값을 publish 하기 위해 sourceQueue를 쓸 것이고 나중에 serialQueue는 scheduler를 바꾸는 실험에 쓸것이다.

  1. timer가 값을 방출하기 위해 Subject를 사용할 것이다. output type 에 대해선 관심없으니 Void 로 해준다.
  2. 11장 타이머에서 배웠듯이 queue들은 time를 만드는데 적합하지만 queue timer를 위한 Publisher API가 없다. 그래서 스케줄러 프로토콜에서 schedule () 메서드의 반복되는 변형을 사용해야한다. 즉시 시작되고 Cancellable을 반환합니다. 타이머가 작동 할 때마다 소스 Subject를 통해 Void 값을 보낸다.

이제 publisher를 달아서 scheduler를 시작해본다.

  1. Timer는 main queue 에서 fire 되고 subject를 통해 Void value를 전달한다.
  2. Publisher는 serial queue에서 값을 받는다.

두번째 recordThread(using:)의 기록을 통해 현재 쓰레드가 receive(on:) 이후에 바꼈는지 보면 DispatchQueue 가 어떤 쓰레드에서 동작할지 전혀 예상할 수 없다는걸 볼수 있다. recevie(on:)과 같은 경우 작업항목은 현재 scheduler 에서 다른 곳으로 hop 하는 값이다.

이제 serial queue 에서 값을 방출하고 receive(on:) 은 동일하게 두면 thread를 계속 바꿀지 알아본다.

또다시 no-thread-guarantee effect of DispatchQueue를 볼 수 있고 또한 receive(on:) operator 가 thread를 바꾸지 않는걸 볼 수 있다. 내부적으로 thread를 추가로 바꾸는 것을 막기위한 최적화가 있어 보이긴 하는데 이번 챕터 challenge 에서 알아보자

DispatchQueue options

DispatchQueue는 operator가 SchedulerOptions 파라미터를 사용할 때 전달할 수있는 옵션 세트를 제공하는 유일한 스케줄러이다.

이러한 옵션은 주로 DispatchQueue에 이미 설정된 값과 독립적으로 QoS (서비스 품질) 값을 정해주는 것 이다.

QoS 말고도 추가적인 flag들이 있는데 대부분의 경우 쓰지 않을 것이다.

QoS를 어떻게 명시하는지 알려면 receive(on:options:) 를 수정한다.

DispatchQueue.SchedulerOptions의 인스턴스를 넘겨서 qos 를 .userInteractive로 설정했다. 이렇게 OS에게 가장 중요한 우선순위를 줄 수 있는데 이 옵션의 경우 UI를 최대한 빨리 업데이트 하기위해 사용한다.

반면에 오래 걸려도 상관없는 경우 .background 옵션을 qos로 사용한다. (이번 예제같은 경우는 하나의 작업만 수행되고 있어서 티나진 않는다.)

실제 애플리케이션에서 이러한 옵션을 사용하면 동시에 많은 대기열이있는 상황에서 먼저 예약 할 작업을 OS가 결정하는 데 도움이 된다. 

OperationQueue

마지막으로 배워볼 scheduler 는 OperationQueue이다.

이 시스템 클래스는 "a queue that regulates the execution of operations" 라고 정의된다.

이 말은 dependencies 가 있는 advanced operation을 만들 수 있는 rich regulation mechanism 이라는 뜻이다.

그러나 Combine의 컨텍스트에서는 이러한 메커니즘을 사용하지 않는다.

OperationQueue는 내부적으로 Dispatch를 사용하기 때문에 둘 중 하나를 사용하는 데 표면적으로 거의 차이가 없다.

간단한 예시를 보면

1과 10 사이 값을 방출하는 간단한 publisher를 만들었다. 그리고 OperationQueue에 값이 전달되도록 했다.

응? 순서가 다르게 나온다. 각 operation이 실행되는 thread를 확인해 보면

각각의 값이 다른 thread 에서 받아진 것을 볼 수 있다. OperationQueue에 대한 문서를 찾아보면 OperationQueue는 Dispatch framework 를 사용해 operation을 실행한다고 되있는데 결국 operation queue 도 같은 쓰레드에서 동작하는것을 보장할 수 없다는 말이다.

더구나 각각의 OperationQueue에는 하나의 파라미터가 있는데 maxConcurrentOperationCount로 디폴트 값은 최대한 많은 operation 을 concurrently 하게 작업하도록 되있다. 그래서 publisher 가 값을 거의 동시에 방출하면 그것들은 다수의 thread에 dispatched 되어서 수행된다.

만약 이 maxConcurrentOperationCount 를 1로 준다면

순서대로 값이 출력되는 것을 볼 수 있다.


근데 나는 왜 1부터 10까지 전부 안나오고 12376만 나오는거지??

10까지 receive되고 그게 OperationQueue로 들어가서 순서만 다르게 출력되야 되는데 왜 7까지 밖에 출력이 안되지??


OperationQueue options

OperationQueue에 사용할 수있는 SchedulerOptions는 없고 실제로 RunLoop.SchedulerOptions로 alias가 지정된 유형이며 자체적으로 옵션을 제공하지 않는다.

OperationQueue pitfalls

OperationQueue가 기본적으로 동시에 작업을 실행하는 것을 확인했다. 이는 문제를 일으킬 수 있으므로이를 잘 알고 있어야 하고 기본적으로 OperationQueue는 동시 DispatchQueue처럼 동작하지만 하지만 publisher가 value를 제공 할 때마다 수행해야 할 중요한 작업이있을 때 좋은 도구가 될 수 있다. maxConcurrentOperationCount 매개 변수를 조정하여 thread 갯수를 제한할 수 있다.

Challenges

1) Stop the timer

DispatchQueue 섹션에서 cancellable timer를 만들어봐라

4초뒤에 멈추는 Timer 를 2가지 방법으로 만들어 봐라

기본 DispatchQueue 코드는 다음과 같다.

let serialQueue = DispatchQueue(label: "Serial queue")
let sourceQueue = DispatchQueue.main

let source = PassthroughSubject<Void, Never>()
let subscription = sourceQueue.schedule(after: sourceQueue.now, interval: .seconds(1)) {
  source.send()
}

let setupPublisher = { recorder in
  return source
    .recordThread(using: recorder)
    .receive(on: serialQueue)
    .recordThread(using: recorder)
    .eraseToAnyPublisher()
}

let view = ThreadRecorderView(title: "Using DispatchQueue", setup: setupPublisher)
PlaygroundPage.current.liveView = UIHostingController(rootView: view)

방법1 : serial queue scheduler protocol인 schedule(after:_:)를 사용

serialQueue.schedule(after: serialQueue.now.advanced(by: .seconds(4))) {
  subscription.cancel()
}

방법2 : serialQueue 보통의 asyncAfter 메소드를 사용

serialQueue.asyncAfter(deadline: .now() + 4) {
  subscription.cancel()
}

2) DIscover optimization

연속적인 receive(on:) call에 같은 scheduler를 사용할 때 Combine은 최적화를 해주는건지 아님 Dispatch framework가 최적화를 해주는지 확실하지 않다.

이 문제에 대한 정답을 찾아내는 메소드를 만드는게 이번 challenge내용이다.

Dispatch framework 에는 DIspatchQueue 의 생성자가 optional target parameter를 받는다. 이 파라미터는 어느 queue 에서 코드를 실행할지 정할 수 있다. 다시말해 우리가 만든 queue는 그림자일 뿐이고 코드가 실행되는 실제 queue는 이 target queue 인 것이다.

그래서 Combine 또는 Dispatch가 최적화를 수행하는지 추측하는 방법은 하나가 다른 하나를 대상으로하는 두 개의 서로 다른 queue를 사용하는 것이다. 따라서 Dispatch 프레임 워크 수준에서 코드는 모두 동일한 대기열에서 실행되지만 (희망적으로) Combine은 인식하지 못한다.

--> DispatchQueue 에서는 target을 통해 두개의 queue가 사실은 하나의 queue에서 실행되게 하고 Combine 한테는 두개의 queue를 사용한다고 recieve(on:)을 통해 말해준다. DispatchQueue 는 두개의 queue 가 사실은 하나의 queue인 것을 알지만 Combine 쪽에서는 다른 queue로 인식을 할 것 이다. 이때 실행해봤을 때 만약 동일 쓰레드에서 값이 수신되면 Dispatch 가 최적화를 수행하고 있는 것이다. (왜냐면 Dispatch 는 같은 queue를 사용하는걸 알기 때문) 근데 다른 쓰레드 값이 수신되면 Combine 이 최적화를 수행하고 있는 것이다.

따라서 이 작업을 수행하고 동일한 스레드에서 수신되는 모든 값을 확인하면 Dispatch가 최적화를 수행하고있을 가능성이 크다.

솔루션을 코딩하는 단계는 다음과 같이

  1. 두번째 serial queue를 만들어 첫번째 queue를 target 으로 한다.
  2. .receive(on:)을 두번째 serial queue에 붙이고 recordThread도 붙인다.

결과는

같은 thread가 나오기 때문에 DispatchQueue 가 최적화를 수행하는걸 알 수 있다.

Keypoint

  • scheduler 는 작업의 context를 정의한다. (which thread)
  • Apple OS는 schedule code execution에 대한 다양한 툴들을 제공한다.
  • Combine은 scheduler의 상단에서 Scheduler protocol 과 함께 어떤 상황에서 어떤 context 가 가장 적합한지 고르는걸 도와준다.
  • 매번 receive(on:)을 사용할 떄 마다 후에 publisher의 operator는 정해진  해당 scheduler에서 실행된다.

 

 

 

 

728x90

'iOS > Combine' 카테고리의 다른 글

Creating custom combine Publisher(Operator)  (0) 2021.10.04
#18 Custom Publishers & Handling Backpressure  (0) 2020.11.03
#Chapter15 In Practice: Combine & SwiftUI  (0) 2020.11.02
#12 Key-Value Observing  (0) 2020.10.30
#9 Networking  (0) 2020.10.28

댓글