📜 ⬆️ ⬇️

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



The mobile application 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 scope of its functions. As the number and complexity of tasks increase, they require more and more time. If the application architecture assumes that all of them are executed in the main thread, then business logic tasks begin to compete over time with interface rendering tasks. With this approach, sooner or later a script is necessarily located, the execution of which leads to the application sticking. To combat this scourge, there are three fundamentally different approaches:

  1. Optimization of algorithms and data structures involved in the execution of the problem scenario.
  2. Removal of the problem scenario from the main thread.
  3. Removal from the main stream of all functions of the application, except for the actual rendering of the user interface.

The SchedulableObject pattern allows you to accurately implement the third script. Under the cat are considered parts of it with examples of implementation on Swift, as well as advantages and disadvantages compared to the first two approaches.

Formulation of the problem


It is considered to be a smooth interface that can be updated at least 60 times per second. These figures can 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 rendered in 10 ms. This means that all business logic tasks should have time to be completed in 6.7 ms.

Consider as an example the business logic of adding a file to the Cloud. It consists of many stages, the essence of which in the context of this article does not interest us.



It is important that all of them together take 2.6 ms. Dividing the maximum time allotted for business logic to work while adding one file, we get 3. So, 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 the 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:

  1. Multiple selection of files from the system gallery of the device.
  2. Automatic download of new files from the gallery.

At the moment, to avoid long-term application hangs, the speed of adding files to the download queue by the autoload service is artificially limited by the shameful constants method. Here are two of them:

//     static uint const kMRCCameraUploaderBatchSize = 1000; static NSTimeInterval const kMRCCameraUploaderBatchDelaySec = 5; 

Their semantics is 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 seconds. But even with this limitation, we have a hang of 1000 * 2.6 ms = 2.6 s every 5 s, which is bound to upset. Artificial restriction of business logic bandwidth - this is the very symptom that indicates the need to look in the direction of the SchedulableObject pattern.

SchedulableObject vs algorithms


Why not solve the problem by optimizing algorithms and data structures? I admit, with a perseverance worthy of a better application, when everything gets really bad, we optimize certain steps involved in adding photos to the download 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. First, for any optimization there will necessarily be such a data stream, which levels all its effect. In relation to the Cloud, this is a user with 20 thousand photos and 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, you will have to optimize the previously performed optimization. Thirdly, loading is not the only scenario whose bandwidth is artificially limited. The expansion of bottlenecks in an algorithmic way will require an individual approach to each scenario. Worse, the quality attribute “Performance” is the antagonist of another, more important, in my opinion, called “Sustainability”. Often, to achieve the required performance, you must either go for all sorts of tricks in the algorithms, or choose more complex data structures, or both. Any choice will not delay negatively affect either the public interface of classes, or at least their internal implementation.

SchedulableObject vs selection script in a separate thread


Let us consider the shortcomings of the approach, in which for each scenario its own decision is made about the expediency of separating it into a separate stream. To this end, we will follow the evolution of the architecture of some business applications, where we will be guided by this principle to solve the problem of “brakes”. Since there are particularly interesting threads (threads) in the context of which the methods of objects are called, each of them will be coded by its own color. Initially, when heavy scripts have not yet appeared, everything happens in the main thread, so all links and entities have the same blue color.



Let us assume that a scenario has appeared, during the development of which one of the objects began to consume too much time. Because of this, the responsiveness of the user interface is starting to suffer. Denote the problem data flow (data flow) bold arrows.



Without refactoring of a resource-intensive class, it is impossible to make calls to it in a separate red stream.



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



As the project progresses, another component becomes a bottleneck. Fortunately, he has only one client, and no thread-safe implementation is 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 quite deplorable.



The disadvantages of the presented approach with the conditional name Thread-Safe Architecture are as follows:

  1. It is necessary to constantly monitor connections between objects for timely refactoring of a single-threaded implementation of a method or class to a thread-safe (and vice versa).
  2. 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.
  3. The active use of synchronization primitives may ultimately make the application even slower than its single-threaded implementation.

The principle of the SchedulableObject pattern


In the world of server, desktop, and even Android, heavy business logic is often separated into a separate process. The interaction between the 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 AS IS does not fit this architecture. However, it can be reproduced in miniature, replacing a separate process with a separate thread, and the interprocess communication mechanism with indirect challenges.



More formally, the essence of transformation is:

  1. Start one separate workflow.
  2. To associate with it an event processing cycle and a special object for delivering messages to it - the scheduler (from the English scheduler).
  3. Associate each variable object with one of the planners. The more objects will be associated with workflow planners, the more time the main thread will have on its main responsibility - drawing the user interface.
  4. Choose the right way to interact objects with each other, depending on their belonging to the scheduler. If the scheduler is common, then the interaction occurs by a direct call of methods, if not, then indirectly, by sending specialized events.

The proposed approach has already been adopted by the iOS community. This is what the high-level architecture of Facebook's popular React Native framework looks like.



All JavaScript code is executed in a separate thread, and interaction with the native code occurs via indirect calls by sending messages through the asynchronous bridge.

Components of the SchedulableObject Pattern


The SchedulableObject pattern is based on five components. Below, for each of them, a zone of responsibility is defined and a naive implementation is proposed in order to most vividly illustrate its internal structure.

Developments


The most convenient abstraction for events in iOS are the blocks within which the required object method is invoked.

 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 on all the difficulties of multi-threaded development of applied 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 loop


Implements strictly sequential event handling from the queue. This property of the component ensures that all calls to the objects implementing it are made in one strictly defined stream.

 class RunLoop { let eventQueue = EventQueue() var disposed = false @objc func run() { while !disposed { for event in eventQueue.resetEvents() { event() } Thread.sleep(forTimeInterval: 0.1) } } } 

In iOS SDK there is a standard implementation of this component - NSRunLoop.

Flow


The kernel object of the operating system in which the message loop code 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 the queue from the Grand Central Dispatch.

Scheduler


Provides a mechanism for delivering events to the desired queue. As the main component through which client code executes object methods, it gives the name of both the SchedulableObject micro pattern and the Schedulable Architecture macro pattern.

 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. With respect to the target object, it can act as an aggregate, as in the example below, and as the base class, as in the POSSchedulableObject library.

 class SchedulableObject<T> { 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) } } } 

We put everything together


The program below duplicates the characters entered into the console. The layer of business logic that we want to remove from the main thread is represented by the Assembly class. It creates and provides access to two services:

  1. Printer prints the lines it feeds to the console.
  2. 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<PrintOptionsProvider> let printer: SchedulableObject<Printer> init() { let optionsProvider = PrintOptionsProvider() self.printOptionsProvider = SchedulableObject<PrintOptionsProvider>( object: optionsProvider, scheduler: backgroundScheduler); self.printer = SchedulableObject<Printer>( 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, if desired, can be simplified:

 assembly.backgroundScheduler.schedule { assembly.printOptionsProvider.object.richFormatEnabled = arc4random() % 2 == 0 assembly.printer.object.doWork(what: value) } 

Rules of interaction with schedulable objects


The above program clearly demonstrates two rules of interaction with schedulable objects.

  1. If the same scheduler is associated with the object client and the called object, then the method is called in the usual way. So, Printer communicates directly with PrintOptionsProvider.
  2. If different planners are associated with the object's client and the called object, then the call occurs indirectly by sending an event. In the example above, the while loop reads user input, executing in the main application thread, and therefore cannot directly access the business logic objects. He interacts with them indirectly - through sending events.

Full listing of the application is available here .

Disadvantages of the SchedulableObject Pattern


With all the elegance of the pattern, he has a dark side: high invasiveness. All is well when the Schedulable Architecture is laid during the initial design, as in this demo application , and the matter takes a completely different turn when life forces to implement it into the existing volume code base. The N-stream nature of the pattern gives rise to two stringent requirements with far-reaching consequences.

Requirement # 1: Immutable Models


All entities moving between threads must be either immutable or schedulable. Otherwise, the whole range of problems of competitive change in their condition does not slow down to wait. 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 vivid materials on this topic:


However, in the code bases of our day, we are more likely to encounter mutable models. Moreover, readwrite properties are the only way to update them when using persistence frameworks like Core Data or Realm. The implementation of Schedulable Architecture makes you either abandon them or provide some special mechanisms for working with models. So, the Realm team offers the following: “ Therefore, you can’t pass the realm objects between threads. If you need the same data for another thread . With Core Data, there are also workarounds, but, in my opinion, this is all very inconvenient and looks like a “thing from the side,” which I don’t want to put into architecture at the design stage. Not so long ago, Facebook in the article “ Making News Feed nearly 50% faster on iOS ” announced its rejection of Core Data. LinkedIn, referring to the same Core Data deficiency, recently introduced its persistent data storage framework: “I ’ve got it ”.

Requirement No. 2: Clusters of Services


Migrating to a separate stream only makes sense when the entire cluster of objects is ready for this. If the services involved in different scenarios live in different streams, then an abundance of indirect calls between them will provoke an incredible code blast.



Now in the 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 increase the number of services implementing the SchedulableObject pattern. As soon as their number reaches a critical mass, sufficient for the implementation of "heavy" scenarios, they will be assigned a workflow planner at a time, and the brakes due to business logic will be a thing of the past.

POSSchedulableObject library


The POSSchedulableObject library is the key ingredient for fully implementing the Schedulable Architecture pattern in the Mail.Ru Cloud iOS application. Despite the fact that the code base is still preparing for the transformation 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 actively used now. One of the key tasks is tracking unauthorized direct calls to object methods from “enemy” flows for it. Not once or twice, POSSchedulableObject informed us with an assertion that we are trying to access the business logic service from a certain workflow. A common reason is the vain hopes that if in iOS 9 the completion-blocks of class methods from the iOS SDK are twitching in the main application thread, then in iOS 10 this contract will not change.

A feature of the implementation of the call detection mechanism from an incorrect flow is that it can be used separately from the POSSchedulableObject class. We used this property to check that the calls of our ViewController methods occur only in the main thread. It looks like this.

 @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 

More information about the library can be found 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 shown as part of the listings of the components of the pattern.

Conclusion


The SchedulableObject pattern provides a systematic approach for moving the business logic of an application out of the main thread. The resulting Schedulable Architecture scales well for two reasons. First, the number of worker threads does not depend on the number of services. Secondly, the entire complexity of multi-threaded development has been transferred from application classes to infrastructure ones. The architecture also has interesting hidden features. For example, we can take out business logic not in one flow, but in several flows. 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 thread in which the message processing loop of the business account of the current account is executed, we can thereby intensify the execution of the most relevant tasks for the user.

Links


  1. Playground with demonstration of the main components of the SchedulableObject pattern
  2. POSSchedulableObject library
  3. De-application using the POSSchedulableObject library

Source: https://habr.com/ru/post/317440/


All Articles