RunLoop vs DispatchQueue в качестве планировщика

При использовании новой платформы Combine вы можете указать планировщик, по которому следует получать элементы от издателя.

Есть ли большая разница между RunLoop.main а также DispatchQueue.main в этом случае при назначении издателя для элемента пользовательского интерфейса? Первая возвращает цикл выполнения основного потока, а вторая очередь связана с основным потоком.

5 ответов

Решение

Я разместил аналогичный вопрос на форуме Swift. Я призываю вас посмотреть обсуждение https://forums.swift.org/t/runloop-main-or-dispatchqueue-main-when-using-combine-scheduler/26635.

Я просто копирую и вставляю ответ от Philippe_Hausler

RunLoop.main как планировщик завершается вызовом RunLoop.main.perform, тогда как DispatchQueue.main вызывает DispatchQueue.main.async для выполнения работы, для практических целей они почти изоморфны. Единственная реальная разница заключается в том, что вызов RunLoop заканчивается тем, что он выполняется в другом месте в сносках RunLoop, тогда как вариант DispatchQueue, возможно, будет выполнен немедленно, если оптимизация в libdispatch включится. На самом деле вы никогда не увидите разницу между этими двумя.

RunLoop должен быть, когда у вас есть выделенный поток с запущенным RunLoop, DispatchQueue может быть любым сценарием очереди (и для записи, пожалуйста, избегайте запуска RunLoops в DispatchQueues, это вызывает некоторое действительно грубое использование ресурсов...). Также стоит отметить, что DispatchQueue, используемый в качестве планировщика, всегда должен быть последовательным, чтобы придерживаться контрактов операторов комбайна.

На самом деле существует большая разница между использованием RunLoop.main как Scheduler и используя DispatchQueue.main как Scheduler:

  • RunLoop.main выполняет обратные вызовы только тогда, когда основной цикл выполнения выполняется в .defaultрежим, который не используется при отслеживании событий касания и мыши.
  • DispatchQueue.main выполняет обратные вызовы во всех .common режимы, которые включают режимы, используемые при отслеживании событий касания и мыши.

Детали

Мы видим реализацию RunLoopсоответствие Scheduler в Schedulers+RunLoop.swift. В частности, вот как он реализуетschedule(options:_:):

    public func schedule(options: SchedulerOptions?,
                         _ action: @escaping () -> Void) {
        self.perform(action)
    }

Это использует RunLoop perform(_:) метод, который является методом Objective-C -[NSRunLoop performBlock:]. ВperformBlock:метод планирует запуск блока только в режиме цикла выполнения по умолчанию. (Это не задокументировано.)

UIKit и AppKit запускают цикл выполнения в режиме по умолчанию в режиме ожидания. Но, в частности, при отслеживании взаимодействия с пользователем (например, касания или нажатия кнопки мыши) они запускают цикл выполнения в другом, нестандартном режиме. Итак, конвейер объединения, который используетreceive(on: RunLoop.main) не будет доставлять сигналы, пока пользователь касается или перетаскивает.

Мы видим реализацию DispatchQueueсоответствие Schedulerв Schedulers+DispatchQueue.swift. Вот как это реализуетсяschedule(options:_:):

    public func schedule(options: SchedulerOptions?, _ action: @escaping () -> Void) {
        let qos = options?.qos ?? .unspecified
        let flags = options?.flags ?? []
        
        if let group = options?.group {
            // Distinguish on the group because it appears to not be a call-through like the others. This may need to be adjusted.
            self.async(group: group, qos: qos, flags: flags, execute: action)
        } else {
            self.async(qos: qos, flags: flags, execute: action)
        }
    }

Таким образом, блок добавляется в очередь с использованием стандартного метода GCD, async(group:qos:flags:execute:). При каких обстоятельствах выполняются блоки в основной очереди? В обычном приложении UIKit или AppKit основной цикл выполнения отвечает за опорожнение основной очереди. Мы можем найти реализацию цикла выполнения в CFRunLoop.c. Важная функция__CFRunLoopRun, который слишком велик, чтобы цитировать его целиком. Вот интересные направления:

#if __HAS_DISPATCH__
    __CFPort dispatchPort = CFPORT_NULL;
    Boolean libdispatchQSafe =
        pthread_main_np()
        && (
            (HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode)
           || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))
        );
    if (
        libdispatchQSafe
        && (CFRunLoopGetMain() == rl)
        && CFSetContainsValue(rl->_commonModes, rlm->_name)
    )
        dispatchPort = _dispatch_get_main_queue_port_4CF();
#endif

(Я обернул исходные строки исходного кода для удобства чтения.) Вот что делает этот код: безопасно ли слить основную очередь, и это основной цикл выполнения, и это .common режим, тогда CFRunLoopRunпроверит, готова ли основная очередь к сливу. В противном случае он не будет проверять и не опустошит основную очередь.

В .commonрежимы включают режимы слежения. Итак, конвейер объединения, который используетreceive(on: DispatchQueue.main) будет подавать сигналы, пока пользователь касается или перетаскивает.

Я увидел ответ, отправленный Роем, и подумал, что могу использовать их как взаимозаменяемые, но на самом деле я заметил большую разницу в своем приложении.

Я загружал изображение асинхронно в ячейку пользовательского представления таблицы. С помощьюRunLoop.main будет блокировать загрузку изображений до тех пор, пока просматривается таблица.

  subscriber = NetworkController.fetchImage(url: searchResult.artworkURL)
    .receive(on: RunLoop.main)
    .replaceError(with: #imageLiteral(resourceName: "PlaceholderArtwork"))
    .assign(to: \.image, on: artworkImageView)

Но переходя на DispatchQueue.main позволяет изображениям загружаться во время прокрутки.

  subscriber = NetworkController.fetchImage(url: searchResult.artworkURL)
    .receive(on: DispatchQueue.main)
    .replaceError(with: #imageLiteral(resourceName: "PlaceholderArtwork"))
    .assign(to: \.image, on: artworkImageView)

Важное предостережение RunLoopв том, что он "не совсем потокобезопасен" (см. https://developer.apple.com/documentation/foundation/runloop), поэтому его можно использовать для задержки выполнения блоков, но не для отправки их из другого потока. Если вы выполняете многопоточную работу (например, загружаете изображение асинхронно), вам следует использоватьDispatchQueue чтобы вернуться к основному потоку пользовательского интерфейса

Runloop.main может потерять свой сигнал в некоторых случаях, например при прокрутке. В большинстве случаев можно использовать DispatchQueue.main~

Другие вопросы по тегам