How does RACScheduler encapsulate GCD in Reactive Cocoa

Posted by beedie on Tue, 25 Jun 2019 19:51:09 +0200


Original address: http://www.jianshu.com/p/980ffdf3ed8c



Preface

In the process of using ReactiveCocoa, Josh Abernathy and Justin Spahr-Summers In order to enable RAC users to immerse themselves in the world of FRP and to program concurrently better, the two gods encapsulated the GCD and integrated it perfectly with the major components of RAC.

Since RACScheduler, it has made the whole RAC concurrent programming code more harmonious, more convenient and more "Reactive Cocoa".

Catalog

  • 1. How does RACScheduler encapsulate GCD
  • 2. Some subclasses of RACSequence
  • 3. How RACScheduler "cancels" concurrent tasks
  • 4. How RACScheduler integrates perfectly with other RAC components

I. How does RACScheduler encapsulate GCD

What exactly is RACScheduler doing in Reactive Cocoa? In what position? The official definition is as follows:

Schedulers are used to control when and where work is performed

RACScheduler is used in Reactive Cocoa to control when and where a task is executed. It is mainly used to solve the problem of concurrent programming in Reactive Cocoa.

The essence of RACScheduler is the encapsulation of GCD, and the bottom layer is the implementation of GCD.

To analyze RACScheduler, first review GCD.


As we all know, in GCD, Dispatch Queue is mainly divided into two categories, Serial Dispatch Queue and Concurrent Dispatch Queue. Serial Dispatch Queue is a queue waiting for the end of processing in current execution, and Concurrent Dispatch Queue is a queue not waiting for the end of processing in current execution.

There are also two ways to generate Dispatch Queue. The first way is to generate Dispatch Queue through the API of GCD.

Generating Serial Dispatch Queue

dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.gcd.SerialDispatchQueue", DISPATCH_QUEUE_SERIAL);

Generating Concurrent Dispatch Queue

dispatch_queue_t concurrentDispatchQueue = dispatch_queue_create("com.gcd.ConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

The second method is to directly obtain the Dispatch Queue provided by the system. The system also provides two categories, Main Dispatch Queue and Global Dispatch Queue. Main Dispatch Queue corresponds to Serial Dispatch Queue and Global Dispatch Queue corresponds to Concurrent Dispatch Queue.

Global Dispatch Queue is divided into eight categories.


Firstly, there are four kinds, which are the cases of priority corresponding to Qos.

  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND

The second is whether overcommit is supported. With the above four priorities, there are eight Global Dispatch Queue s. The queue with overcommit indicates that whenever a task is submitted, the system will open a new thread to process, so that no overcommit will occur.


Back in RACScheduler, since RACScheduler is the encapsulation of GCD, these types mentioned above also have one-to-one corresponding encapsulation.

typedef enum : long {
     RACSchedulerPriorityHigh = DISPATCH_QUEUE_PRIORITY_HIGH,
     RACSchedulerPriorityDefault = DISPATCH_QUEUE_PRIORITY_DEFAULT,
     RACSchedulerPriorityLow = DISPATCH_QUEUE_PRIORITY_LOW,
     RACSchedulerPriorityBackground = DISPATCH_QUEUE_PRIORITY_BACKGROUND,
} RACSchedulerPriority;

First of all, the priority in RACScheduler, which encapsulates only four kinds, is also corresponding to DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_PRIORITY_DEFAULT, DISPATCH_QUEUE_PRIORITY_LOW, DISPATCH_QUEUE_PRIORITY_LOW, DISPATCH_QUEUE_PRIORITY_BACKGROUND.

RACScheduler has six class methods that are used to generate a queue.

+ (RACScheduler *)immediateScheduler;
+ (RACScheduler *)mainThreadScheduler;

+ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name;
+ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority;
+ (RACScheduler *)scheduler;

+ (RACScheduler *)currentScheduler;

Next, analyze their underlying implementations in turn.


1. immediateScheduler

+ (instancetype)immediateScheduler {
    static dispatch_once_t onceToken;
    static RACScheduler *immediateScheduler;
    dispatch_once(&onceToken, ^{
        immediateScheduler = [[RACImmediateScheduler alloc] init];
    });

    return immediateScheduler;
}

The underlying implementation of immediate Scheduler is to generate a RACImmediate Scheduler instance.

RACImmediate Scheduler is inherited from RACScheduler.

@interface RACImmediateScheduler : RACScheduler
@end

In RACScheduler, each type of RACScheduler has a name attribute, and the name is their tag. The name of RACImmediate Scheduler is @ "com. ReactiveCocoa. RACScheduler. immediate Scheduler"

RACImmediate Scheduler acts like its name, performing tasks in closures immediately.

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    block();
    return nil;
}

- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
    NSCParameterAssert(date != nil);
    NSCParameterAssert(block != NULL);

    [NSThread sleepUntilDate:date];
    block();

    return nil;
}

In the schedule: method, the execution of the parameter block() closure is called directly. In the after: schedule: method, the thread first sleeps until the date, and then wakes up to execute the parameterized block() closure.

- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
    NSCAssert(NO, @"+[RACScheduler immediateScheduler] does not support %@.", NSStringFromSelector(_cmd));
    return nil;
}

Of course, RACImmediate Scheduler cannot support after: repeating Every: with Leeway: schedule: method. Because its definition is immediate, it should not be repeat.

- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock {

    for (__block NSUInteger remaining = 1; remaining > 0; remaining--) {
        recursiveBlock(^{
            remaining++;
        });
    }
    return nil;
}

RACImmediate Scheduler's scheduleRecursiveBlock: In the method, as long as the recursiveBlock closure exists, the call will be executed indefinitely recursively unless the recursiveBlock does not exist.

2. mainThreadScheduler

MaiThreadScheduler is also an example of a type RACTargetQueue Scheduler.

+ (instancetype)mainThreadScheduler {
    static dispatch_once_t onceToken;
    static RACScheduler *mainThreadScheduler;
    dispatch_once(&onceToken, ^{
        mainThreadScheduler = [[RACTargetQueueScheduler alloc] initWithName:@"com.ReactiveCocoa.RACScheduler.mainThreadScheduler" targetQueue:dispatch_get_main_queue()];
    });

    return mainThreadScheduler;
}

The name of mainThreadScheduler is @ "com.ReactiveCocoa.RACScheduler.mainThreadScheduler".

RACTargetQueue Scheduler inherits from RACQueue Scheduler

@interface RACTargetQueueScheduler : RACQueueScheduler
- (id)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue;
@end

In RACTargetQueue Scheduler, there is only one initialization method.

- (id)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue {
    NSCParameterAssert(targetQueue != NULL);

    if (name == nil) {
        name = [NSString stringWithFormat:@"com.ReactiveCocoa.RACTargetQueueScheduler(%s)", dispatch_queue_get_label(targetQueue)];
    }

    dispatch_queue_t queue = dispatch_queue_create(name.UTF8String, DISPATCH_QUEUE_SERIAL);
    if (queue == NULL) return nil;

    dispatch_set_target_queue(queue, targetQueue);

    return [super initWithName:name queue:queue];
}

First, a new queue is created with the name @ "com.ReactiveCocoa.RACScheduler.mainThreadScheduler", which is of Serial Dispatch Queue type, and then the dispatch_set_target_queue method is called.

So the emphasis is on the dispatch_set_target_queue method.

The dispatch_set_target_queue method has two main purposes: one is to set the priority of dispatch_queue_create to create queues, and the other is to establish the execution level of queues.

  • When dispatch_queue_create is used to create queues, their priority is at the DISPATCH_QUEUE_PRIORITY_DEFAULT level, whether serial or parallel, and this API can set the priority of queues.

For instance:

dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//Note: The queue with priority is the first parameter.
dispatch_set_target_queue(serialQueue, globalQueue);

With the above code, set serailQueue to DISPATCH_QUEUE_PRIORITY_HIGH.

  • Using this dispatch_set_target_queue method, queue execution levels can be set, such as dispatch_set_target_queue(queue, targetQueue);
    This setting is equivalent to assigning queue to targetQueue, if targetQueue is a serial queue, then queue is serial; if targetQueue is a parallel queue, then queue is parallel.

For instance:

    dispatch_queue_t targetQueue = dispatch_queue_create("targetQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);

    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);

    dispatch_async(queue1, ^{
        NSLog(@"queue1 1");
    });
    dispatch_async(queue1, ^{
        NSLog(@"queue1 2");
    });
    dispatch_async(queue2, ^{
        NSLog(@"queue2 1");
    });
    dispatch_async(queue2, ^{
        NSLog(@"queue2 2");
    });
    dispatch_async(targetQueue, ^{
        NSLog(@"target queue");
    });

If the targetQueue is Serial Dispatch Queue, the output must be as follows:

queue1 1
queue1 2
queue2 1
queue2 2
target queue

If targetQueue is Concurrent Dispatch Queue, the output may be as follows:

queue1 1
queue2 1
queue1 2
target queue
queue2 2

Back in RACTargetQueue Scheduler, the incoming parameter here is dispatch_get_main_queue(), which is a Serial Dispatch Queue, where the dispatch_set_target_queue method is called, which corresponds to the consistency of queue priority setting with main_queue.

3. scheduler

The three methods are essentially the same.

+ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name;
+ (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority;
+ (RACScheduler *)scheduler;
+ (instancetype)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name {
    return [[RACTargetQueueScheduler alloc] initWithName:name targetQueue:dispatch_get_global_queue(priority, 0)];
}

+ (instancetype)schedulerWithPriority:(RACSchedulerPriority)priority {
    return [self schedulerWithPriority:priority name:@"com.ReactiveCocoa.RACScheduler.backgroundScheduler"];
}

+ (instancetype)scheduler {
    return [self schedulerWithPriority:RACSchedulerPriorityDefault];
}

From the source code, we can see that the three methods of scheduler series are to create a Global Dispatch Queue, which corresponds to Concurrent Dispatch Queue.

SchdulerWithPriority: name: The method specifies the priority and name of the thread.

SchdulerWithPriority: Method can only execute priority with the default name @ "com.ReactiveCocoa.RACScheduler.backgroundScheduler".

The queue created by the scheduler method has the default priority and the default name @ "com.ReactiveCocoa.RACScheduler.backgroundScheduler".

Note that the difference between scheduler and mainThreadScheduler and immediate Scheduler is that each time the scheduler creates a new Concurrent Dispatch Queue.

4. currentScheduler

+ (instancetype)currentScheduler {
    RACScheduler *scheduler = NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey];
    if (scheduler != nil) return scheduler;
    if ([self.class isOnMainThread]) return RACScheduler.mainThreadScheduler;
    return nil;
}

First, a key @ "RACScheduler Current Scheduler Key" is defined in Reactive Cocoa, which is used to access the corresponding RACScheduler from the thread dictionary.

NSString * const RACSchedulerCurrentSchedulerKey = @"RACSchedulerCurrentSchedulerKey";

In the current Scheduler method, you see a RACScheduler taken from the thread dictionary. As for when it will be saved, it will be explained below.

If a RACScheduler can be retrieved from the thread dictionary, the retrieved RACScheduler is returned. If not in the dictionary, then determine whether the current scheduler is on the main thread.

+ (BOOL)isOnMainThread {
    return [NSOperationQueue.currentQueue isEqual:NSOperationQueue.mainQueue] || [NSThread isMainThread];
}

Judgment method as above, as long as the NSOperationQueue on the mainQueue, or NSThread is the main thread, are considered to be on the main thread.

If it is on the main thread, it returns mainThreadScheduler.
If neither the main thread nor the value corresponding to the key value can be found in the thread dictionary, then nil is returned.

Apart from six class methods, RACScheduler has four instance methods:

- (RACDisposable *)schedule:(void (^)(void))block;
- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block;
- (RACDisposable *)afterDelay:(NSTimeInterval)delay schedule:(void (^)(void))block;
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block;

These four methods are actually known by name for what they are used for.

schedule: Adding a task to RACScheduler, the entry is a closure.

after: schedule: Adds a timed task to RACScheduler, which is executed after date time.

After delay: schedule: Adds a delayed task for RACScheduler, which is executed after delaying time.

After: repeating Every: withLeeway: schedule: Add a timed task for RACScheduler, start after date time, and then execute the task every interval second.

These four methods are rewritten in each subclass of RACScheduler.

For example, the immediateScheduler, schedule: method executes closures directly and immediately. after: schedule: A timed task is added to the method to execute the task after date time. After: repeating Every: withLeeway: schedule: This method returns nil directly in RACImmediate Scheduler.

There are other subclasses that will analyze the implementation of these four methods below.

There are also the last three methods

- (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock;
- (void)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock addingToDisposable:(RACCompoundDisposable *)disposable
- (void)performAsCurrentScheduler:(void (^)(void))block;

The first two methods are to implement signalWithScheduler in RACSequence: the method. For specific analysis, see This article

PerfmAsCurrent Scheduler: The method is used in RACQueue Scheduler, which is analyzed in detail in the following subclass analysis.

Some subclasses of RACSequence

RACSequence has five subclasses in total.


1. RACTestScheduler

This class is mainly a test class, mainly used in unit testing, it is used to verify that asynchronous calls do not spend a lot of time waiting. RACTest Scheduler can also be used in multithreading, where only one method can be selected to execute at a time in a queued method queue.

@interface RACTestSchedulerAction : NSObject
@property (nonatomic, copy, readonly) NSDate *date;
@property (nonatomic, copy, readonly) void (^block)(void);
@property (nonatomic, strong, readonly) RACDisposable *disposable;

- (id)initWithDate:(NSDate *)date block:(void (^)(void))block;
@end

In unit testing, ReactiveCocoa creates a new RACTestScheduler Action object to facilitate comparing and describing the whole process of testing. The definition of RACTest Scheduler Action is as follows. Now let's explain the parameters.

date is a time to compare and decide which closure to start executing next time.

void (^block)(void) closure is a task in RACScheduler.

disposable controls whether an action can be executed. Once disposed, the action will not be executed.

initWithDate: block: The method is to initialize a new action.

During unit testing, step method needs to be invoked to see how closures are invoked each time.

- (void)step {
    [self step:1];
}

- (void)stepAll {
    [self step:NSUIntegerMax];
}

Both step and step All methods call step: methods. Step performs only one task in RACScheduler, and step All performs all tasks in RACScheduler. Now that they all call step:, let's analyze the implementation of step:.

- (void)step:(NSUInteger)ticks {
    @synchronized (self) {
        for (NSUInteger i = 0; i < ticks; i++) {
            const void *actionPtr = NULL;
            if (!CFBinaryHeapGetMinimumIfPresent(self.scheduledActions, &actionPtr)) break;

            RACTestSchedulerAction *action = (__bridge id)actionPtr;
            CFBinaryHeapRemoveMinimumValue(self.scheduledActions);

            if (action.disposable.disposed) continue;

            RACScheduler *previousScheduler = RACScheduler.currentScheduler;
            NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self;

            action.block();

            if (previousScheduler != nil) {
                NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler;
            } else {
                [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey];
            }
        }
    }
}

step: The main implementation is a for loop. The number of cycles is determined by ticks. First const void * action Ptr is a pointer to a function. One important function in the above implementation is CFBinaryHeapGetMinimumIfPresent. The prototype of the function is as follows:

Boolean CFBinaryHeapGetMinimumIfPresent(CFBinaryHeapRef heap, const void **value)

The main function of this function is to find a minimum in the heap of the binary heap.

static CFComparisonResult RACCompareScheduledActions(const void *ptr1, const void *ptr2, void *info) {
    RACTestSchedulerAction *action1 = (__bridge id)ptr1;
    RACTestSchedulerAction *action2 = (__bridge id)ptr2;
    return CFDateCompare((__bridge CFDateRef)action1.date, (__bridge CFDateRef)action2.date, NULL);
}

The rule of comparison is as above, which is to compare the value of date between the two. Finding such a minimum from the binary heap corresponds to the tasks in the scheduler. If the minimum has several equal minimum values, a minimum value is returned randomly. The returned function is placed in actionPtr. The return value of the whole function is a BOOL value. If the binary heap is not empty, the minimum value can be found and YES will be returned. If the binary heap is empty, the minimum value can not be found and NO will be returned.

NSUIntegerMax is introduced into the step All method, and the for loop will not die because CFBinaryHeapGetMinimumIfPresent returns NO after all tasks in the heap have been executed, and break s are executed to jump out of the loop.

The current Scheduler is saved here in the thread dictionary. Then action.block is executed to perform tasks.

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != nil);

    @synchronized (self) {
        NSDate *uniqueDate = [NSDate dateWithTimeIntervalSinceReferenceDate:self.numberOfDirectlyScheduledBlocks];
        self.numberOfDirectlyScheduledBlocks++;

        RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:uniqueDate block:block];
        CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);

        return action.disposable;
    }
}

- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
    NSCParameterAssert(date != nil);
    NSCParameterAssert(block != nil);

    @synchronized (self) {
        RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:block];
        CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);

        return action.disposable;
    }
}

schedule: The numberOfDirectly Scheduled Blocks value is accumulated in the method, which is also initialized to time to compare the scheduled time of each method. NumberOfDirectly Scheduled Blocks eventually represent how many block tasks have been generated in total. Then CFBinaryHeapAddValue is added to the heap.

after:schedule: Create the RACTestScheduler Action object directly, and then add the block closure to the heap with CFBinaryHeapAddValue.

After: repeating Every: withLeeway: schedule: Also a new RACTestScheduler Action object, which is then added to the heap with CFBinaryHeapAddValue.

2. RACSubscriptionScheduler

RACSubscription Scheduler is the last single RACScheduler. The only three singletons in RACScheduler are now complete: RACImmediate Scheduler, RACTargetQueue Scheduler, and RACSubscription Scheduler.

+ (instancetype)subscriptionScheduler {
    static dispatch_once_t onceToken;
    static RACScheduler *subscriptionScheduler;
    dispatch_once(&onceToken, ^{
        subscriptionScheduler = [[RACSubscriptionScheduler alloc] init];
    });

    return subscriptionScheduler;
}

The name of RACSubscription Scheduler is @ "com. ReactiveCocoa. RACScheduler. subscription Scheduler"

- (id)init {
    self = [super initWithName:@"com.ReactiveCocoa.RACScheduler.subscriptionScheduler"];
    if (self == nil) return nil;
    _backgroundScheduler = [RACScheduler scheduler];  
    return self;
}

RACSubscription Scheduler initializes with a new Global Dispatch Queue.

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);
    if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block];
    block();
    return nil;
}

If RACScheduler. current Scheduler is nil, the back groundScheduler is used to call the block closure, otherwise the block closure is executed.

- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
    RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler;
    return [scheduler after:date schedule:block];
}
- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
    RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler;
    return [scheduler after:date repeatingEvery:interval withLeeway:leeway schedule:block];
}

Both after methods take out RACScheduler. current Scheduler and use self.backgroundScheduler to call their after methods if they are empty.

This is the meaning of backgroundScheduler in RACSubscription Scheduler, which replaces self.backgroundScheduler when RACScheduler. current Scheduler does not exist.

3. RACImmediateScheduler

This subclass has been analyzed in detail when analyzing the immediateScheduler method, which is not discussed here.

4. RACQueueScheduler

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    RACDisposable *disposable = [[RACDisposable alloc] init];

    dispatch_async(self.queue, ^{
        if (disposable.disposed) return;
        [self performAsCurrentScheduler:block];
    });

    return disposable;
}

schedule: The performAsCurrentScheduler: method is called.

- (void)performAsCurrentScheduler:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    RACScheduler *previousScheduler = RACScheduler.currentScheduler;
    NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self;

    @autoreleasepool {
        block();
    }

    if (previousScheduler != nil) {
        NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler;
    } else {
        [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey];
    }
}

PerfmAsCurrentScheduler: The method stores the current scheduler in the thread dictionary before calling block().

Imagine that in a Concurrent Dispatch Queue, you need to switch threads to the current scheduler before executing block(). After the block closure is executed, if previous Scheduler is not nil, restore the scene, save the original scheduler in the thread dictionary, and vice versa, remove the key in the thread dictionary.

It is worth noting here that:

Schduler is essentially a quene, not a thread. It can only ensure that the threads inside are executed serially, but it can not guarantee that each thread is not necessarily executed in the same thread.

As shown in the implementation of performAsCurrent Scheduler: above. therefore
Using Core Data in scheduler is easy to crash and is likely to run on sub-threads. Once it's time to write data to the sub-threads, it's easy to Crash. Make sure you go back to the main queue.

- (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
    NSCParameterAssert(date != nil);
    NSCParameterAssert(block != NULL);

    RACDisposable *disposable = [[RACDisposable alloc] init];

    dispatch_after([self.class wallTimeWithDate:date], self.queue, ^{
        if (disposable.disposed) return;
        [self performAsCurrentScheduler:block];
    });

    return disposable;
}

The dispatch_after method is called in after, and after date time performAsCurrentScheduler:.

WallTime WithDate: Implemented as follows:

+ (dispatch_time_t)wallTimeWithDate:(NSDate *)date {
    NSCParameterAssert(date != nil);

    double seconds = 0;
    double frac = modf(date.timeIntervalSince1970, &seconds);

    struct timespec walltime = {
        .tv_sec = (time_t)fmin(fmax(seconds, LONG_MIN), LONG_MAX),
        .tv_nsec = (long)fmin(fmax(frac * NSEC_PER_SEC, LONG_MIN), LONG_MAX)
    };

    return dispatch_walltime(&walltime, 0);
}

The dispatch_walltime function obtains the value of dispatch_time_t from the time of struct timespec type used in POSIX. The dispatch_time function is usually used to calculate relative time, while the dispatch_walltime function is used to calculate absolute time.

This code is actually very simple, which converts the date's time into a dispatch_time_t type. The value of dispatch_time_t type that can be passed to the dispatch_after function is obtained from the NSDate class object.

- (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
    NSCParameterAssert(date != nil);
    NSCParameterAssert(interval > 0.0 && interval < INT64_MAX / NSEC_PER_SEC);
    NSCParameterAssert(leeway >= 0.0 && leeway < INT64_MAX / NSEC_PER_SEC);
    NSCParameterAssert(block != NULL);

    uint64_t intervalInNanoSecs = (uint64_t)(interval * NSEC_PER_SEC);
    uint64_t leewayInNanoSecs = (uint64_t)(leeway * NSEC_PER_SEC);

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
    dispatch_source_set_timer(timer, [self.class wallTimeWithDate:date], intervalInNanoSecs, leewayInNanoSecs);
    dispatch_source_set_event_handler(timer, block);
    dispatch_resume(timer);

    return [RACDisposable disposableWithBlock:^{
        dispatch_source_cancel(timer);
    }];
}

After: repeating Every: withLeeway: schedule: The implementation of the method is to create a Timer on self.queue with GCD, the interval is time interval, and the correction time is leeway.

This parameter, leeway, specifies an expected timer event precision for dispatch source, allowing the system to manage and wake up the kernel flexibly. For example, the system can use the leeway value to pre-trigger or delay the timer to better integrate with other system events. When creating your own timer, you should try to specify a leeway value. But even if you specify a leeway value of 0, you can't fully expect the timer to trigger events in precise nanoseconds.

This timer executes the join closure in interval. Call dispatch_source_cancel to cancel the timer when the task is cancelled.

5. RACTargetQueueScheduler

This subclass has been analyzed in detail in the analysis of the mainThreadScheduler method, which will not be repeated here.

3. How RACScheduler "cancels" concurrent tasks


Since RACScheduler is the encapsulation of GCD, some "features" that GCD can't accomplish can be realized at the top of GCD. The "features" here are quoted, because the bottom layer is GCD, and the features of the upper layer can only be achieved by some special means, which seems to be a new feature. At this point, RACScheduler implements the "Cancel" task, a feature that GCD does not have.

Operation Queues :
Comparing with GCD, using Operation Queues can increase a little extra overhead, but in return it has a very powerful flexibility and function. It can add dependencies between operations, cancel an ongoing operation, pause and restore operation queue, etc.

GCD:
It is a lightweight way to execute concurrent tasks in FIFO sequence. When using GCD, we can not care about the scheduling of tasks, but let the system handle them automatically. But the drawbacks of GCD are also very obvious. It becomes very difficult to add dependencies between tasks, cancel or suspend an ongoing task.

Since GCD is not convenient to cancel a task, how does RACScheduler do it?

This is reflected in RACQueue Scheduler. Look back at RACQueue Scheduler's schedule: implementation and after: schedule: implementation.

Core code:

 dispatch_async(self.queue, ^{
      if (disposable.disposed) return;
      [self performAsCurrentScheduler:block];
 });

Before calling performAsCurrentScheduler: a judgment is added to determine whether the task is cancelled or not. If the task is cancelled, it return s and the block closure is not called. In this way, the "illusion" of canceling tasks is realized.

IV. How RACScheduler integrates perfectly with other RAC components

Throughout Reactive Cocoa, many operations are implemented using RACScheduler, which is deeply integrated with RAC. Here's a summary of where RACScheduler is used in Reactive Cocoa.


Search RACScheduler globally in ReactiveCocoa and traverse all libraries. RACScheduler is used in the following 10 classes. Now let's see how it's used in these places.

Using Scheduler in the following places, we can see which operations are in sub-threads and which are in main threads. Distinguishing these, for thread unsafe operations, we can handle them adequately, let them return to the main thread to operate, so that we can reduce a lot of inexplicable Crash. These Crash are all caused by threading problems.

1. In RACCommand
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock

This method is very complex, using RACScheduler. immediate Scheduler, deliverOn:RACScheduler.mainThreadScheduler. Specific source code analysis will be detailed in the next RACCommand source code analysis.

- (RACSignal *)execute:(id)input

In this method, subscribeOn:RACScheduler.mainThreadScheduler is called.

2. In RACDynamic Signal
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    NSCParameterAssert(subscriber != nil);

    RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
    subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable];

    if (self.didSubscribe != NULL) {
        RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
            RACDisposable *innerDisposable = self.didSubscribe(subscriber);
            [disposable addDisposable:innerDisposable];
        }];

        [disposable addDisposable:schedulingDisposable];
    }

    return disposable;
}

Subscribe Scheduler is used in the subscribe: subscribe process of RACDynamic Signal. Calling schedule on this scheduler executes the following code:

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block];

    block();
    return nil;
}

If the current Scheduler is not empty, the closure will be executed in the current Scheduler, and if the current Scheduler is empty, the closure will be executed in the backgroundScheduler, which is a Global Dispatch Queue with priority RACScheduler Priority Default.

Similarly, subscription Scheduler is also invoked in subscriptions to RACEmpty signal, RACErrorSignal, RACReturnSignal, and RACSignal related signals.

3. In RACBehavior Subject
- (RACDisposable *)subscribe:(id<RACSubscriber>)subscriber {
    RACDisposable *subscriptionDisposable = [super subscribe:subscriber];

    RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{
        @synchronized (self) {
            [subscriber sendNext:self.currentValue];
        }
    }];

    return [RACDisposable disposableWithBlock:^{
        [subscriptionDisposable dispose];
        [schedulingDisposable dispose];
    }];
}

Subscribe scheduler is used in the subscribe: subscribe process of RACBehaviorSubject. So we call schedule:, which is analyzed above.

Similarly, if the current Scheduler is not empty, the closure will be executed in the current Scheduler, and if the current Scheduler is empty, the closure will be executed in the backgroundScheduler, which is a Global Dispatch Queue with priority RACScheduler Priority Default.

4. In RACReplaySubject

Its subscription, like the subscription for the signal above, calls subscription Scheduler.

Since RACReplaySubject is on a sub-thread, it is recommended that you remember to add delivery on when using unsafe libraries such as Core Data.

5. In RACSequence

In RACSequence, the following two methods use RACScheduler:

- (RACSignal *)signal {
    return [[self signalWithScheduler:[RACScheduler scheduler]] setNameWithFormat:@"[%@] -signal", self.name];
}
- (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler {
    return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        __block RACSequence *sequence = self;

        return [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) {
            if (sequence.head == nil) {
                [subscriber sendCompleted];
                return;
            }

            [subscriber sendNext:sequence.head];

            sequence = sequence.tail;
            reschedule();
        }];
    }] setNameWithFormat:@"[%@] -signalWithScheduler: %@", self.name, scheduler];
}

The above two methods call the scheduleRecursiveBlock: method in RACScheduler. Source code analysis of this method can be seen Source Code Analysis of RACSequence.

6. In RACSignal+Operations

There are nine methods that use Scheduler.

The first method is:

static RACDisposable *subscribeForever (RACSignal *signal, void (^next)(id), void (^error)(NSError *, RACDisposable *), void (^completed)(RACDisposable *))

It's used in the above method.

RACScheduler *recursiveScheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler];

Take out the current Scheduler or a Global Dispatch Queue and call scheduleRecursiveBlock:.

The second method is:

- (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id next))predicate

Called in the above method

RACScheduler *scheduler = [RACScheduler scheduler];
RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler

Calling afterDelay: schedule: method in delayScheduler is also an important step in the implementation of throttle:valuesPassingTest: method.

The third method:

- (RACSignal *)delay:(NSTimeInterval)interval

Since this is a delayed method, Scheduler's after method is definitely called.

   RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler;
   RACDisposable *schedulerDisposable = [delayScheduler afterDelay:interval schedule:block];

RACScheduler. current Scheduler?: Schduler's method of judging the time-related factors is used.

So here's a suggestion:
Since delay does not necessarily go back to the current thread, it may be executed in the sub-thread after delay to subscribe. So it's better to add a delivery On when using delay.

The fourth method:

- (RACSignal *)bufferWithTime:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler

This method naturally needs to call the method [scheduler after Delay: interval schedule: flushValues] to achieve the purpose of delay, so as to achieve the effect of buffer.

Fifth method:

+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler

The sixth method:

+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway { }

The fifth method and the sixth method call after: repeating Every: withLeeway: schedule: method with the incoming reference scheduler.

The seventh method:

- (RACSignal *)timeout:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { }

In this method, the parameter scheduler is used to call afterDelay: schedule:, which is delayed for a period of time and then executes [disposable dispose], thus realizing the timeout sendError:.

The eighth method:

- (RACSignal *)deliverOn:(RACScheduler *)scheduler { }

The ninth method:

- (RACSignal *)subscribeOn:(RACScheduler *)scheduler { }

Both the eighth method and the ninth method call schedule: method based on the participation scheduler. What type of scheduler to participate in determines the schedule: which queue to execute.

7. In RACSignal

There are also active computational and inert evaluation signals in RACSignal.

+ (RACSignal *)startEagerlyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id<RACSubscriber> subscriber))block {
    NSCParameterAssert(scheduler != nil);
    NSCParameterAssert(block != NULL);

    RACSignal *signal = [self startLazilyWithScheduler:scheduler block:block];
    [[signal publish] connect];
    return [signal setNameWithFormat:@"+startEagerlyWithScheduler: %@ block:", scheduler];
}

Start Eagerly With Scheduler is called to generate a signal signal, which is then converted into a thermal signal. The signal generated by start Eagerly With Scheduler is a direct thermal signal.

+ (RACSignal *)startLazilyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id<RACSubscriber> subscriber))block {
    NSCParameterAssert(scheduler != nil);
    NSCParameterAssert(block != NULL);

    RACMulticastConnection *connection = [[RACSignal
                                           createSignal:^ id (id<RACSubscriber> subscriber) {
                                               block(subscriber);
                                               return nil;
                                           }]
                                          multicast:[RACReplaySubject subject]];

    return [[[RACSignal
              createSignal:^ id (id<RACSubscriber> subscriber) {
                  [connection.signal subscribe:subscriber];
                  [connection connect];
                  return nil;
              }]
             subscribeOn:scheduler]
            setNameWithFormat:@"+startLazilyWithScheduler: %@ block:", scheduler];
}

The above is the source implementation of startLazily WithScheduler: in this method, the biggest difference between startEagerly WithScheduler and the connect method is in the return signal, so Lazily is reflected in the signal created by startLazily WithScheduler, which can only be invoked to connect after subscribing to it and turned into a hot signal.

subscribeOn:scheduler is called here, and scheduler is used here.

8. In NSData+RACSupport
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler {
    NSCParameterAssert(scheduler != nil);

    RACReplaySubject *subject = [RACReplaySubject subject];
    [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ options: %lu scheduler: %@", URL, (unsigned long)options, scheduler];

    [scheduler schedule:^{
        NSError *error = nil;
        NSData *data = [[NSData alloc] initWithContentsOfURL:URL options:options error:&error];
        if (data == nil) {
            [subject sendError:error];
        } else {
            [subject sendNext:data];
            [subject sendCompleted];
        }
    }];

    return subject;
}

In this method, RACQueue Scheduler or RACTargetQueue Scheduler's RAC Scheduler is passed in. Calling the schedule method then executes here:

- (RACDisposable *)schedule:(void (^)(void))block {
    NSCParameterAssert(block != NULL);

    RACDisposable *disposable = [[RACDisposable alloc] init];

    dispatch_async(self.queue, ^{
        if (disposable.disposed) return;
        [self performAsCurrentScheduler:block];
    });

    return disposable;
}
9. In NSString+RACSupport
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL usedEncoding:(NSStringEncoding *)encoding scheduler:(RACScheduler *)scheduler {
    NSCParameterAssert(scheduler != nil);

    RACReplaySubject *subject = [RACReplaySubject subject];
    [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ usedEncoding:scheduler: %@", URL, scheduler];

    [scheduler schedule:^{
        NSError *error = nil;
        NSString *string = [NSString stringWithContentsOfURL:URL usedEncoding:encoding error:&error];
        if (string == nil) {
            [subject sendError:error];
        } else {
            [subject sendNext:string];
            [subject sendCompleted];
        }
    }];

    return subject;
}

Like rac_readContentsOfURL: options: scheduler: in NSData+RACSupport, RACQueue Scheduler or RACTargetQueue Scheduler's RAC Scheduler is also passed in.

10. In NSUser Defaults + RACSupport
RACScheduler *scheduler = [RACScheduler scheduler];

A new RACTargetQueue Scheduler, a Global Dispatch Queue, will also be created in this method. The priority is RACScheduler Priority Default.

Last

The analysis of the underlying implementation of RACScheduler has been completed. Finally, please give us more advice.

Topics: Programming encoding Attribute