
Using the SchedulableObject pattern to separate business logic into a separate thread

The mobile app interface is the face of the product. The more responsive the interface, the more joy the product brings. However, satisfaction with the use of the application depends primarily on the volume of its functions. As the number and complexity of tasks increase, they require more and more time. If the architecture of the application assumes that they are all executed in the main thread, then the tasks of business logic begin to compete over time with the tasks of rendering the interface. With this approach, sooner or later a script is necessarily found, the execution of which leads to an application sticking. To combat this scourge, there are three fundamentally different approaches:
- Optimization of algorithms and data structures involved in the execution of the problem scenario.
- Removing a problematic scenario from the main thread.
- Removal from the main thread of all application functions, with the exception of the actual user interface rendering.
The SchedulableObject pattern allows you to accurately implement the third scenario. Under the cut, its parts with examples of implementation on Swift are considered, as well as advantages and disadvantages compared to the first two approaches.
Formulation of the problem
It is considered to be smooth such an interface that can be updated at least 60 times per second. These figures can also be viewed from the other side:

It turns out that each event processing cycle should have time to complete in 16.7 ms. Suppose a user observes a window that can be drawn in 10 ms. This means that all business logic tasks must be completed in 6.7 ms.


The important thing is that they all together take 2.6 ms. Dividing the maximum time allotted for the work of business logic by adding one file, we get 3. Therefore, if the application wants to remain responsive when working out this scenario, it cannot add more than three files at a time. Fortunately for the user, but unfortunately for developers, there are cases in the Cloud application when you need to add more than three files at a time.

The screenshots above show some of them:
- Multiple selection of files from the system gallery of the device.
- Automatically download new files from the gallery.
At the moment, in order to avoid prolonged suspension of the application, the speed of adding files to the queue for downloading by the startup service is artificially limited by the method of shameful constants. Here are two of them:
// Позорные константы сервиса автозагрузки
static uint const kMRCCameraUploaderBatchSize = 1000;
static NSTimeInterval const kMRCCameraUploaderBatchDelaySec = 5;
Their semantics are as follows: according to the results of scanning the gallery for new photos, the service should add them to the download queue in batches of no more than 1000 pieces each with an interval of 5 s. But even with this limitation, we have a hang for 1000 * 2.6 ms = 2.6 s every 5 s, which cannot but upset. The artificial limitation of the bandwidth of business logic is the very symptom that indicates the need to look towards the SchedulableObject pattern.
SchedulableObject vs algorithms
Why not solve the problem of optimizing algorithms and data structures to solve the problem? I admit, with perseverance worthy of a better application, when everything becomes really bad, we optimize certain steps involved in adding photos to the upload queue. However, the potential of these efforts is deliberately limited. Yes, you can twist something and increase the size of the pack to 2 or even 4 thousand pieces, but this does not solve the problem fundamentally. Firstly, for any optimization there is sure to be a data stream that eliminates its entire effect. In relation to the Cloud, this is a user with 20 thousand photos or more in the gallery. Secondly, the management will definitely want to make your script even more intelligent, which will inevitably lead to a complication of its logic, it will be necessary to optimize the previously performed optimization. Thirdly, Downloading is not the only scenario whose throughput is artificially limited. Aligning bottlenecks in an algorithmic way will require an individual approach to each scenario. To make matters worse, the attribute “Performance” is an antagonist of another, more important, in my opinion, called “Support”. Often, in order to achieve the required performance, it is necessary either to go to various tricks in the algorithms, or to choose more complex data structures, or both. Any choice will not slow down negatively either on the public interface of classes, or at least on their internal implementation. To make matters worse, the attribute “Performance” is an antagonist of another, more important, in my opinion, called “Support”. Often, in order to achieve the required performance, it is necessary either to go to various tricks in the algorithms, or to choose more complex data structures, or both. Any choice will not slow down negatively either on the public interface of classes, or at least on their internal implementation. To make matters worse, the attribute “Performance” is an antagonist of another, more important, in my opinion, called “Support”. Often, in order to achieve the required performance, it is necessary either to go to various tricks in the algorithms, or to choose more complex data structures, or both. Any choice will not slow down negatively either on the public interface of classes, or at least on their internal implementation.
SchedulableObject vs script allocation in a separate thread
Consider the shortcomings of the approach in which each scenario makes its own decision on the appropriateness of separating it into a separate stream. To this end, we will follow the evolution of the architecture of a certain business application, where we will be guided by this principle to solve the “brakes” problem. Since threads are particularly interesting here, in the context of which methods of objects are called, each of them will be encoded in its own color. Initially, when heavy scenarios have not yet appeared, everything happens in the main stream, so all connections and entities have the same blue color.

Suppose a scenario has appeared during which one of the objects began to consume too much time. Because of this, the responsiveness of the user interface begins to suffer. Denote the problematic data flow by bold arrows.

Without refactoring a resource-intensive class, you cannot make calls to it in a separate red stream.

The reason is that he has not one client, but two, and the second still makes calls from the main, blue thread. If these calls change the shared state of an object, then the classic data race problem will occur. To protect against it, it is necessary to implement a red object in a thread-safe manner.

As the project develops, another component becomes a bottleneck. Fortunately, he has only one client, and a thread-safe implementation was not required.

If you extrapolate this approach to the design of multi-threaded architecture, sooner or later it comes to the next state.

With a little complication, it becomes completely deplorable.

The disadvantages of the approach with the code name Thread-Safe Architecture are as follows:
- It is necessary to constantly monitor the connections between objects for timely refactoring of a single-threaded implementation of a method or class to thread-safe (and vice versa).
- Thread-safe methods are difficult to implement, because, in addition to applied logic, it is necessary to take into account the specifics of multi-threaded programming.
- Active use of synchronization primitives can ultimately make the application even slower than its single-threaded implementation.
SchedulableObject Pattern Principle
In the world of server, desktop, and even Android development, heavy business logic is often separated into a separate process. The interaction between services within each of the processes remains single-threaded. Services from different processes interact with each other using various interprocess communication mechanisms (COM / DCOM, Corba, .Net Remoting, Boost.Interprocess, etc.).

Unfortunately, in the world of iOS development, we are limited to only one process, and such an architecture is not suitable for AS IS. However, it can be reproduced in miniature, replacing a separate process with a separate thread, and the mechanism of interprocess interaction with indirect calls.

More formally, the essence of the transformation is as follows:
- Start one separate workflow.
- Associate with it an event processing cycle and a special object for delivering messages to it - the scheduler (from the English scheduler).
- Associate each mutable object with one of the schedulers. The more objects associated with the workflow schedulers, the more time the main thread will have left for its main responsibility - rendering the user interface.
- Choose the right way for objects to interact with each other, depending on their affiliation with the schedulers. If the scheduler is general, then the interaction occurs by direct method invocation, if not, then indirectly, via sending specialized events.
The proposed approach has already been adopted by the iOS community. This is what the high-level architecture of the popular React Native framework from Facebook looks like.

All JavaScript code is executed in a separate thread, and interaction with native code occurs through indirect calls by sending messages via asynchronous bridge.
SchedulableObject Pattern Components
The SchedulableObject pattern is based on five components. Below, for each of them, a zone of responsibility is determined and a naive implementation is proposed in order to most graphically illustrate its internal structure.
Events
The most convenient abstraction for events in iOS is the blocks inside which the required method of the object is called.
typealias Event = () -> Void
Event queue
Since the events in the queue come from different threads, the queue requires a thread-safe implementation. In fact, it is she who takes care of all the difficulties of multithreaded development from application components.
class EventQueue {
private let semaphore = DispatchSemaphore(value: 1)
private var events = [Event]()
func pushEvent(event: @escaping Event) {
semaphore.wait()
events.append(event)
semaphore.signal()
}
func resetEvents() -> [Event] {
semaphore.wait()
let currentEvents = events
events = [Event]()
semaphore.signal()
return currentEvents
}
}
Message processing cycle
Implements strictly sequential processing of events from the queue. This property of the component ensures that all calls to the objects that implement it are made in one, strictly defined thread.
class RunLoop {
let eventQueue = EventQueue()
var disposed = false
@objc func run() {
while !disposed {
for event in eventQueue.resetEvents() {
event()
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
The iOS SDK has a standard implementation of this component - NSRunLoop.
Flow
The kernel object of the operating system in which the code for the message processing loop is executed. The lowest-level implementation in the iOS SDK is the NSThread class. For practical purposes, it is recommended to use higher-level primitives like NSOperationQueue or queues from Grand Central Dispatch.
Scheduler
Provides a mechanism for delivering events to the desired queue. Being the main component through which client code executes object methods, it gives the name to both the SchedulableObject micropattern and the Schedulable Architecture macropattern.
class Scheduler {
private let runLoop = RunLoop()
private let thread: Thread
init() {
self.thread = Thread(target:runLoop,
selector:#selector(RunLoop.run),
object:nil)
thread.start()
}
func schedule(event: @escaping Event) {
runLoop.eventQueue.pushEvent(event: event)
}
func dispose() {
runLoop.disposed = true
}
}
Schedulableobject
Provides a standard interface for indirect calls. In relation to the target, it can act as both an aggregate, as in the example below, and a base class, as in the POSSchedulableObject library .
class SchedulableObject {
private let object: T
private let scheduler: Scheduler
init(object: T, scheduler: Scheduler) {
self.object = object
self.scheduler = scheduler
}
func schedule(event: @escaping (T) -> Void) {
scheduler.schedule {
event(self.object)
}
}
}
Putting it all together
The program below duplicates the characters entered into the console in the console. The layer of business logic that we want to pull out of the main thread is represented by the Assembly class. It creates and provides access to two services:
- Printer prints the lines it feeds to the console.
- PrintOptionsProvider allows you to configure the Printer service.
//
// main.swift
// SchedulableObjectDemo
//
class PrintOptionsProvider {
var richFormatEnabled = false;
}
class Printer {
private let optionsProvider: PrintOptionsProvider
init(optionsProvider: PrintOptionsProvider) {
self.optionsProvider = optionsProvider
}
func doWork(what: String) {
if optionsProvider.richFormatEnabled {
print("\(Thread.current): out \(what)")
} else {
print("out \(what)")
}
}
}
class Assembly {
let backgroundScheduler = Scheduler()
let printOptionsProvider: SchedulableObject
let printer: SchedulableObject
init() {
let optionsProvider = PrintOptionsProvider()
self.printOptionsProvider = SchedulableObject(
object: optionsProvider,
scheduler: backgroundScheduler);
self.printer = SchedulableObject(
object: Printer(optionsProvider: optionsProvider),
scheduler: backgroundScheduler)
}
}
let assembly = Assembly()
while true {
guard let value = readLine(strippingNewline: true) else {
continue
}
if (value == "q") {
assembly.backgroundScheduler.dispose()
break;
}
assembly.printOptionsProvider.schedule(
event: { (printOptionsProvider: PrintOptionsProvider) in
printOptionsProvider.richFormatEnabled = arc4random() % 2 == 0
})
assembly.printer.schedule(event: { (printer: Printer) in
printer.doWork(what: value)
})
}
The last block of code can be simplified if desired:
assembly.backgroundScheduler.schedule {
assembly.printOptionsProvider.object.richFormatEnabled = arc4random() % 2 == 0
assembly.printer.object.doWork(what: value)
}
Rules for interacting with schedulable objects
The above program demonstrates two rules for interacting with schedulable objects.
- If the same scheduler is associated with the client of the object and with the called object, then the method is called in the usual way. So, Printer communicates directly with PrintOptionsProvider.
- If different schedulers are associated with the client of the object and with the called object, then the call is made indirectly by sending an event. In the example above, the while loop reads user input, executing in the main thread of the application, and therefore cannot directly access business logic objects. He interacts with them indirectly - through sending events.
A complete listing of the application is available here .
Disadvantages of the SchedulableObject Pattern
Despite the elegance of the pattern, it also has a dark side: high invasiveness. All is well when the Schedulable Architecture is laid down during the initial design, as in this demo application , and the case takes a completely different turn when life forces you to embed it in the existing voluminous code base. The N-threaded nature of the pattern gives rise to two stringent requirements with far-reaching consequences.
Requirement No. 1: Immutable Models
All entities moving between threads must be either immutable or schedulable. Otherwise, the whole range of problems of competitive changes in their state will not slow down. Today, there is a clear trend towards the use of immutable model objects. At its forefront are companies that are faced with the need to isolate business logic from the main stream. Here is a list of perhaps the most striking materials on this topic:
- Apple WWDC: Building Better Apps with Value Types in Swift
- Facebook: ComponentKit ... emphasizes a one-way data flow from immutable models to immutable components
- Dropbox: Practical Cross-Platform Mobile C ++ Development
- Pinterest: Immutable models and data consistency in our iOS App
- LinkedIn: Managing Consistency of Immutable Models
However, in the codebases of our days, we are likely to encounter mutable models. Moreover, readwrite properties are the only way to update them when using persistence frameworks such as Core Data or Realm. The introduction of Schedulable Architecture forces either to abandon them, or to provide for some special mechanisms for working with models. So, the Realm team offers the following: “ Therefore, the only limitation with Realm is that you cannot pass Realm objects between threads. If you need the same data on another thread, you just query for that data on the other thread". With Core Data, there are also workarounds, but, in my opinion, it’s all very inconvenient and looks like a “side view” that you don’t really want to lay in the architecture at the design stage. Not so long ago, Facebook in its article “ Making News Feed nearly 50% faster on iOS ” announced its abandonment of Core Data. LinkedIn, citing the same drawback of Core Data, recently introduced its framework for persistent data storage: " Rocket Data is a better option to Core Data because of the speed and stability guarantees as well as working with immutable instead of mutable models ."
Requirement No. 2: Clusters of Services
Migration to a separate stream makes sense only when the entire cluster of objects is ready for this. If the services participating in different scenarios live in different flows, then the abundance of indirect calls between them will provoke code blast of incredible proportions.

Now in Mail.Ru Cloud, as part of product development, we are gradually preparing business logic for life outside the main stream. So, with each release, we are increasing the number of services that implement the SchedulableObject pattern. As soon as their number reaches a critical mass sufficient to implement “difficult” scenarios, they will be set up at the same time as a workflow scheduler, and the brakes due to business logic will be a thing of the past.
POSSchedulableObject Library
POSSchedulableObject Library- a key ingredient for the full implementation of the Schedulable Architecture pattern in the Mail.Ru Cloud iOS application. Despite the fact that the code base is just getting ready to transform from a single-threaded state to a two-threaded state, refactoring is already beneficial. Since POSSchedulableObject is used as the base class for all managed objects, some of its properties are being actively used now. One of the key ones is tracking unauthorized direct calls to methods of an object from “enemy” flows for it. More than once or twice POSSchedulableObject informed us with an assert that we are trying to access the business logic service from a certain workflow. A common reason is the vain hope that if in iOS 9 completion blocks of class methods from the iOS SDK twitch in the main thread of the application,
A feature of the implementation of the mechanism for detecting calls from an incorrect stream is that it can be used separately from the POSSchedulableObject class. We used this property to verify that method calls to our ViewControllers only happen in the main thread. It looks as follows.
@implementation UIViewController (MRCApp)
- (BOOL)mrc_protectForMainThreadScheduler {
POSScheduleProtectionOptions *options =
[POSScheduleProtectionOptions
include:[POSSchedulableObject
selectorsForClass:self.class
nonatomicOnly:YES
predicate:^BOOL(SEL _Nonnull selector) {
NSString *selectorName = NSStringFromSelector(selector);
return [selectorName rangeOfString:@"_"].location != 0;
}]
exclude:[POSSchedulableObject selectorsForClass:[UIResponder class]]];
return [POSSchedulableObject protect:self
forScheduler:[RACTargetQueueScheduler pos_mainThreadScheduler]
options:options];
}
@end
You will find more information about the library in its description in the repository on GitHub . As soon as we stop supporting iOS 7, we’ll immediately take care of the version for Swift, the sketches of which were demonstrated as part of the listing of the components of the pattern.
Conclusion
The SchedulableObject pattern provides a systematic approach for taking the business logic of an application out of the main thread. The resulting Schedulable Architecture scales well for two reasons. First, the number of workflows is independent of the number of services. Secondly, the whole complexity of multithreaded development is transferred from application classes to infrastructure classes. Architecture also has interesting hidden possibilities. For example, we can take out business logic not in one thread, but in several threads. By changing the priority of each of them, we change at the macro level the intensity of use of system resources by each of the clusters of objects. This can be useful, for example, when implementing multi-account in an application. By increasing the priority of the stream in which the business logic message processing cycle of the current account is executed,