Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RunLoop vs DispatchQueue as Scheduler

When using new Combine framework you can specify the scheduler on which to receive elements from the publisher.

Is there a big difference between RunLoop.main and DispatchQueue.main in this case when assigning publisher to UI element? The first one returns the run loop of the main thread and the second queue associated with the main thread.

like image 969
mikro098 Avatar asked Jun 23 '19 13:06

mikro098


People also ask

What is RunLoop in iOS?

Overview. A RunLoop object processes input for sources, such as mouse and keyboard events from the window system and Port objects. A RunLoop object also processes Timer events. Your application neither creates nor explicitly manages RunLoop objects.

What is DispatchQueue Main?

DispatchQueue.main is an instance of DispatchQueue . All dispatch queues can schedule their work to be executed sync or async .

What is Uikit RunLoop?

Role of a run loop On iOS, a run loop can be attached to a NSThread . Its role is to ensure that its NSThread is busy when there is work to do and at rest when there is none. The main thread automatically launches its run loop at the application launch.

What is dispatch queue in Swift?

Dispatch queues are FIFO queues to which your application can submit tasks in the form of block objects. Dispatch queues execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system.


1 Answers

There actually is a big difference between using RunLoop.main as a Scheduler and using DispatchQueue.main as a Scheduler:

  • RunLoop.main runs callbacks only when the main run loop is running in the .default mode, which is not the mode used when tracking touch and mouse events. If you use RunLoop.main as a Scheduler, your events will not be delivered while the user is in the middle of a touch or drag.

  • DispatchQueue.main runs callbacks in all of the .common modes, which include the modes used when tracking touch and mouse events. If you use DispatchQueue.main, your events will be delivered while the use user in the middle of a touch or drag.

Details

We can see the implementation of RunLoop's conformance to Scheduler in Schedulers+RunLoop.swift. In particular, here's how it implements schedule(options:_:):

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

This uses the RunLoop perform(_:) method, which is the Objective-C method -[NSRunLoop performBlock:]. The performBlock: method schedules the block to run in the default run loop mode only. (This is not documented.)

UIKit and AppKit run the run loop in the default mode when idle. But, in particular, when tracking a user interaction (like a touch or a mouse button press), they run the run loop in a different, non-default mode. So a Combine pipeline that uses receive(on: RunLoop.main) will not deliver signals while the user is touching or dragging.

We can see the implementation of DispatchQueue's conformance to Scheduler in Schedulers+DispatchQueue.swift. Here's how it implements 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)         }     } 

So the block gets added to the queue using a standard GCD method, async(group:qos:flags:execute:). Under what circumstances are blocks on the main queue executed? In a normal UIKit or AppKit app, the main run loop is responsible for draining the main queue. We can find the run loop implementation in CFRunLoop.c. The important function is __CFRunLoopRun, which is much too big to quote in its entirety. Here are the lines of interest:

#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 

(I have wrapped the original source lines for readability.) Here's what that code does: if it's safe to drain the main queue, and it's the main run loop, and it's a .common mode, then CFRunLoopRun will check for the main queue being ready to drain. Otherwise, it will not check and so it will not drain the main queue.

The .common modes include the tracking modes. So a Combine pipeline that uses receive(on: DispatchQueue.main) will deliver signals while the user is touching or dragging.

like image 159
rob mayoff Avatar answered Sep 22 '22 20:09

rob mayoff