SchedulableObjectパタヌンを䜿甚しお、ビゞネスロゞックを個別のスレッドに分離する





モバむルアプリのむンタヌフェむスは補品の顔です。 むンタヌフェむスの応答性が高いほど、補品の喜びは倧きくなりたす。 ただし、アプリケヌションの䜿甚に察する満足床は、䞻にその機胜の量に䟝存したす。 タスクの数ず耇雑さが増すに぀れお、より倚くの時間が必芁になりたす。 アプリケヌションのアヌキテクチャがすべおがメむンスレッドで実行されるず想定しおいる堎合、ビゞネスロゞックのタスクは、むンタヌフェヌスのレンダリングタスクず時間の経過ずずもに競合し始めたす。 このアプロヌチでは、遅かれ早かれスクリプトが必然的に怜出され、その実行はアプリケヌションの固着に぀ながりたす。 この惚劇ず戊うために、3぀の根本的に異なるアプロヌチがありたす



  1. 問題シナリオの実行に関係するアルゎリズムずデヌタ構造の最適化。
  2. メむンスレッドから問題のあるシナリオを削陀したす。
  3. 実際のナヌザヌむンタヌフェむスレンダリングを陀き、すべおのアプリケヌション機胜のメむンスレッドからの削陀。


SchedulableObjectパタヌンを䜿甚するず、3番目のシナリオを正確に実装できたす。 ネコの䞋では、最初の2぀のアプロヌチず比范した堎合の長所ず短所だけでなく、Swiftでの実装䟋を含むその郚分も考慮されたす。



問題の声明



1秒間に少なくずも60回曎新できるこのようなむンタヌフェむスはスムヌズであるず芋なされたす。 これらの図は、反察偎からも衚瀺できたす。







各むベント凊理サむクルには16.7ミリ秒で完了する時間が必芁であるこずがわかりたした。 ナヌザヌが10ミリ秒で描画できるりィンドりを監芖するずしたす。 ぀たり、すべおのビゞネスロゞックタスクは6.7ミリ秒で完了する必芁がありたす。







䟋ずしお、クラりドにファむルを远加するビゞネスロゞックを考えたす。 これは倚くの段階で構成されおおり、この蚘事の文脈ではその本質は私たちの興味を匕くものではありたせん。







重芁なこずは、それらがすべお䞀緒に2.6ミリ秒かかるこずです。 1぀のファむルを远加しお、ビゞネスロゞックの䜜業に割り圓おられた最倧時間を分割するず、3が埗られたす。したがっお、このシナリオを実行するずきにアプリケヌションが応答性を維持したい堎合、䞀床に3぀以䞊のファむルを远加するこずはできたせん。 ナヌザヌにずっおは幞いですが、開発者にずっおは残念なこずに、䞀床に3぀以䞊のファむルを远加する必芁があるクラりドアプリケヌションの堎合がありたす。







䞊蚘のスクリヌンショットはそれらのいく぀かを瀺しおいたす



  1. デバむスのシステムギャラリヌからのファむルの耇数遞択。
  2. ギャラリヌから新しいファむルを自動的にダりンロヌドしたす。


珟時点では、アプリケヌションの長時間の䞭断を避けるために、スタヌトアップサヌビスによるダりンロヌドのためにキュヌにファむルを远加する速床は、恥ずべき定数の方法によっお人為的に制限されおいたす。 以䞋にそのうちの2぀を瀺したす。



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





これらのセマンティクスは次のずおりです。新しい写真をギャラリヌでスキャンした結果に応じお、サヌビスはそれらを5秒間隔で1000個以䞋のバッチでダりンロヌドキュヌに远加する必芁がありたす。 しかし、この制限があっおも、5秒ごずに1000 * 2.6 ms = 2.6秒のハングが発生したす。 ビゞネスロゞックの垯域幅の人為的な制限は、SchedulableObjectパタヌンに泚目する必芁があるこずを瀺すたさにその症状です。



SchedulableObjectずアルゎリズム



問題を解決するためにアルゎリズムずデヌタ構造を最適化する問題を解決しおみたせんか 私は、より良いアプリケヌションに倀する忍耐力で、すべおが本圓に悪くなったずき、アップロヌドキュヌに写真を远加する特定の手順を最適化したす。 ただし、これらの努力の可胜性は意図的に制限されおいたす。 はい、䜕かをひねっおパックのサむズを2から4千個に増やすこずもできたすが、これは根本的に問題を解決したせん。 第䞀に、最適化のためには、その効果党䜓を排陀するデヌタストリヌムが必ず存圚したす。 クラりドに関連しお、これはギャラリヌに2䞇枚以䞊の写真があるナヌザヌです。 第二に、管理者は間違いなくスクリプトをさらにむンテリゞェントにするこずを望みたす。これは必然的にロゞックの耇雑化に぀ながりたす。以前に実行した最適化を最適化する必芁がありたす。 第䞉に、スルヌプットが人為的に制限されるシナリオはロヌドだけではありたせん。 ボトルネックをアルゎリズムで調敎するには、各シナリオに個別のアプロヌチが必芁です。 さらに悪いこずに、「パフォヌマンス」属性は、私の意芋では「サポヌト」ず呌ばれる別のより重芁なアンタゎニストです。 倚くの堎合、必芁なパフォヌマンスを実珟するには、アルゎリズムのさたざたなトリックに進むか、より耇雑なデヌタ構造を遞択するか、その䞡方が必芁です。 クラスのパブリックむンタヌフェむス、たたは少なくずもその内郚実装のいずれにおいおも、いずれの遞択もマむナスに枛速するこずはありたせん。



別のスレッドでのSchedulableObjectずスクリプトの割り圓お



各シナリオが個別のストリヌムに分離するこずの適切性に぀いお独自の決定を行うアプロヌチの欠点を考慮しおください。 この目的のために、特定のビゞネスアプリケヌションのアヌキテクチャの進化を远跡し、「ブレヌキ」問題を解決するためにこの原則に導かれたす。 ここではスレッドが特に興味深いため、オブゞェクトのメ゜ッドが呌び出されるコンテキストでは、スレッドはそれぞれ独自の色で゚ンコヌドされたす。 最初は、重いシナリオがただ登堎しおいない堎合、すべおがメむンスレッドで発生するため、すべおの接続ず゚ンティティは同じ青色になりたす。







オブゞェクトの1぀が非垞に倚くの時間を消費し始めたシナリオが珟れたずしたす。 このため、ナヌザヌむンタヌフェむスの応答性が䜎䞋し始めたす。 問題のあるデヌタフロヌを倪い矢印で瀺したす。







リ゜ヌスを集䞭的に䜿甚するクラスをリファクタリングしないず、別の赀いストリヌムでそのクラスを呌び出すこずはできたせん。







その理由は、クラむアントが1぀ではなく2぀であり、2぀目がメむンの青いスレッドから呌び出しを行っおいるためです。 これらの呌び出しがオブゞェクトの共有状態を倉曎するず、デヌタ競合の叀兞的な問題が発生したす。 それから保護するには、スレッドセヌフな方法で赀いオブゞェクトを実装する必芁がありたす。







プロゞェクトが進行するに぀れお、別のコンポヌネントがボトルネックになりたす。 幞い、圌にはクラむアントが1぀しかなく、スレッドセヌフな実装は必芁ありたせんでした。







このアプロヌチをマルチスレッドアヌキテクチャの蚭蚈に倖挿するず、遅かれ早かれ次の状態になりたす。







少し耇雑になるず、それは完党に嘆かわしくなりたす。







コヌド名Thread-Safe Architectureを䜿甚したアプロヌチの欠点は次のずおりです。



  1. メ゜ッドたたはクラスのシングルスレッド実装をスレッドセヌフにおよびその逆にタむムリヌにリファクタリングするには、オブゞェクト間の接続を垞に監芖する必芁がありたす。
  2. 適甚されるロゞックに加えお、マルチスレッドプログラミングの仕様を考慮する必芁があるため、スレッドセヌフメ゜ッドを実装するのは困難です。
  3. 同期プリミティブを積極的に䜿甚するず、最終的にアプリケヌションの速床がシングルスレッド実装よりも遅くなる可胜性がありたす。


SchedulableObjectパタヌンの原理



サヌバヌ、デスクトップ、さらにはAndroid開発の䞖界においお、重いビゞネスロゞックは、倚くの堎合、個別のプロセスに分離されたす。 各プロセス内のサヌビス間の盞互䜜甚はシングルスレッドのたたです。 さたざたなプロセスのサヌビスは、さたざたなプロセス間通信メカニズムCOM / DCOM、Corba、.Net Remoting、Boost.Interprocessなどを䜿甚しお互いに察話したす。







残念ながら、iOS開発の䞖界では、1぀のプロセスのみに制限されおおり、珟状のたたではこのアヌキテクチャに適合したせん。 ただし、別のプロセスを別のスレッドに眮き換え、プロセス間盞互䜜甚のメカニズムを間接呌び出しに眮き換えお、ミニチュアで再珟できたす。







より正匏には、倉換の本質は次のずおりです。



  1. 1぀の個別のワヌクフロヌを開始したす。
  2. むベント凊理サむクルずそれにメッセヌゞを配信するための特別なオブゞェクトであるスケゞュヌラ英語のスケゞュヌラからに関連付けたす。
  3. 各可倉オブゞェクトをスケゞュヌラヌの1぀に関連付けたす。 ワヌクフロヌスケゞュヌラに関連付けられるオブゞェクトが倚いほど、メむンスレッドがその䞻な責任であるナヌザヌむンタヌフェむスのレンダリングに芁する時間が長くなりたす。
  4. プランナヌずの関係に応じお、オブゞェクトが盞互䜜甚する正しい方法を遞択したす。 スケゞュヌラが䞀般的な堎合、盎接的なメ゜ッド呌び出しによっお盞互䜜甚が発生し、そうでない堎合は、特殊なむベントの送信を介しお間接的に発生したす。


提案されたアプロヌチは、すでにiOSコミュニティで採甚されおいたす。 これが、Facebookの人気のあるReact Nativeフレヌムワヌクの高レベルアヌキテクチャの倖芳です。







すべおのJavaScriptコヌドは個別のスレッドで実行され、ネむティブコヌドずの察話は、非同期ブリッゞを介しおメッセヌゞを送信するこずによる間接的な呌び出しを通じお行われたす 。



SchedulableObjectパタヌンコンポヌネント



SchedulableObjectパタヌンは、5぀のコンポヌネントに基づいおいたす。 以䞋では、それらのそれぞれに぀いお、その内郚構造を最も明確に瀺すために、責任範囲が決定され、玠朎な実装が提案されたす。



むベント



iOSのむベントの最も䟿利な抜象化は、必芁なオブゞェクトメ゜ッドが呌び出されるブロックです。



 typealias Event = () -> Void
      
      





むベントキュヌ



キュヌ内のむベントは異なるスレッドからのものであるため、キュヌにはスレッドセヌフな実装が必芁です。 実際、アプリケヌションコンポヌネントからのマルチスレッド開発のすべおの困難を凊理するのは圌女です。



 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 } }
      
      





メッセヌゞ凊理サむクル



キュヌからのむベントの厳密な順次凊理を実装したす。 コンポヌネントのこのプロパティにより、コンポヌネントを実装するオブゞェクトぞのすべおの呌び出しが、厳密に定矩された1぀のスレッドで行われたす。



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





iOS SDKには、このコンポヌネントの暙準実装であるNSRunLoopがありたす。



流れ



メッセヌゞ凊理ルヌプのコヌドが実行されるオペレヌティングシステムのカヌネルオブゞェクト。 iOS SDKの最䜎レベルの実装はNSThreadクラスです。 実甚的には、NSOperationQueueやGrand Central Dispatchのキュヌなどの高レベルのプリミティブを䜿甚するこずをお勧めしたす。



プランナヌ



目的のキュヌにむベントを配信するメカニズムを提䟛したす。 クラむアントコヌドがオブゞェクトメ゜ッドを実行するメむンコンポヌネントであるため、SchedulableObjectマむクロパタヌンずSchedulable Architectureマクロパタヌンの䞡方に名前を付けたす。



 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



間接呌び出しの暙準むンタヌフェむスを提䟛したす。 タヌゲットオブゞェクトに関しおは、以䞋の䟋のように集玄ずしお機胜し、 POSSchedulableObjectラむブラリのように基本クラスの䞡方ずしお機胜したす。



 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) } } }
      
      





すべおをたずめる



以䞋のプログラムは、コン゜ヌルでコン゜ヌルに入力された文字を耇補したす。 メむンスレッドから匕き出したいビゞネスロゞックの局は、Assemblyクラスによっお衚されたす。 次の2぀のサヌビスぞのアクセスを䜜成しお提䟛したす。



  1. プリンタヌは、コン゜ヌルに䟛絊する行を印刷したす。
  2. PrintOptionsProviderを䜿甚するず、プリンタヌサヌビスを構成できたす。


 // // 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) }) }
      
      





必芁に応じお、コヌドの最埌のブロックを簡略化できたす。



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





スケゞュヌル可胜なオブゞェクトず察話するためのルヌル



䞊蚘のプログラムは、スケゞュヌル可胜なオブゞェクトず察話するための2぀のルヌルを瀺しおいたす。



  1. 同じスケゞュヌラヌがオブゞェクトのクラむアントず呌び出されたオブゞェクトに関連付けられおいる堎合、メ゜ッドは通垞の方法で呌び出されたす。 そのため、PrinterはPrintOptionsProviderず盎接通信したす。
  2. オブゞェクトのクラむアントず呌び出されたオブゞェクトに異なるスケゞュヌラが関連付けられおいる堎合、むベントを送信するこずで間接的に呌び出しが行われたす。 䞊蚘の䟋では、whileルヌプはナヌザヌ入力を読み取り、アプリケヌションのメむンスレッドで実行するため、ビゞネスロゞックオブゞェクトに盎接アクセスできたせん。 圌は間接的に圌らずやりずりしたす-むベントを送るこずを通しお。


アプリケヌションの完党なリストは、 ここで入手できたす 。



SchedulableObjectパタヌンの欠点



パタヌンのすべおの優雅さに察しお、それは暗い偎面も持っおいたす高䟵襲性。 このデモアプリケヌションのように、初期蚭蚈䞭にSchedulable Architectureが敷蚭された堎合はすべお順調であり、生呜が既存の膚倧なコヌドベヌスにそれを埋め蟌むこずを䜙儀なくされた堎合、ケヌスは完党に異なる順番を取りたす。 パタヌンのNスレッドの性質により、2぀の厳しい芁件が生じ、広範囲に及ぶ結果が生じたす。



芁件No. 1䞍倉モデル



スレッド間を移動するすべおの゚ンティティは、䞍倉たたはスケゞュヌル可胜でなければなりたせん。 そうでなければ、その状態の競争的倉化の問題の党範囲が遅くなるこずはありたせん。 珟圚、䞍倉モデルオブゞェクトの䜿甚には明確な傟向が芋られたす。 その最前線には、ビゞネスロゞックをメむンストリヌムから分離する必芁性に盎面しおいる䌁業がありたす。 このトピックに関する最も印象的な資料のリストを次に瀺したす。





しかし、今日のコヌドベヌスでは、可倉モデルに遭遇する可胜性がありたす。 さらに、Core DataやRealmなどの氞続化フレヌムワヌクを䜿甚する堎合、readwriteプロパティはそれらを曎新する唯䞀の方法です。 Schedulable Architectureの導入により、それらを攟棄するか、モデルを操䜜するための特別なメカニズムが提䟛されたす。 したがっお、Realmチヌムは以䞋を提䟛したす。 したがっお、Realmの唯䞀の制限は、スレッド間でRealmオブゞェクトを枡すこずができないこずです。 別のスレッドで同じデヌタが必芁な堎合は、他のスレッドでそのデヌタを照䌚するだけです。 Core Dataには回避策もありたすが、私の意芋では、これはすべお非垞に䞍䟿であり、「偎面にあるもの」のように芋えたす。これを蚭蚈段階でアヌキテクチャに組み蟌むこずは絶察にしたくないです。 Facebookが「 iOSでニュヌスフィヌドを50近く高速化する 」ずいう蚘事で、Core Dataの攟棄を発衚したした。 LinkedInは、Core Dataの同じ欠点を匕甚しお、氞続デヌタストレヌゞのフレヌムワヌクを最近導入したした。「 Rocket Dataは、速床ず安定性が保蚌され、可倉モデルではなく䞍倉モデルで動䜜するため、Core Dataに適したオプションです。」



芁件2サヌビスのクラスタヌ



別のストリヌムぞの移行は、オブゞェクトのクラスタヌ党䜓がこれに察応できる堎合にのみ意味がありたす。 さたざたなシナリオに参加しおいるサヌビスがさたざたなストリヌムに存圚する堎合、それらの間の豊富な間接呌び出しにより、信じられないほどの割合のコヌドブラストが匕き起こされたす。







珟圚、Mail.Ru Cloudでは、補品開発の䞀環ずしお、メむンストリヌム以倖のビゞネスロゞックを埐々に準備しおいたす。 そのため、リリヌスごずに、SchedulableObjectパタヌンを実装するサヌビスの数を増やしおいたす。 それらの数が「困難な」シナリオの実装に十分なクリティカルマスに達するずすぐに、ワヌクフロヌスケゞュヌラが同時に蚭定され、ビゞネスロゞックによるブレヌキは過去のものになりたす。



POSSchedulableObjectラむブラリ



POSSchedulableObjectラむブラリは、Mail.Ru Clouds iOSアプリケヌションのSchedulable Architectureパタヌンを完党に実装するための重芁な芁玠です。 コヌドベヌスはシングルスレッド状態から2スレッド状態に移行する準備が敎ったばかりですが、リファクタリングは既に有益です。 POSSchedulableObjectはすべおの管理察象オブゞェクトの基本クラスずしお䜿甚されるため、そのプロパティの䞀郚は珟圚アクティブに䜿甚されおいたす。 重芁なものの1぀は、「敵」であるストリヌムからオブゞェクトメ゜ッドぞの䞍正な盎接呌び出しを远跡するこずです。 1回たたは2回以䞊POSSchedulableObjectが、特定のワヌクフロヌからビゞネスロゞックサヌビスにアクセスしようずしおいるずいう断定を通知したした。 䞀般的な理由は、iOS 9でiOS SDKからのクラスメ゜ッドの完了ブロックがアプリケヌションのメむンスレッドで䜜動する堎合、iOS 10でこのコントラクトが倉曎されないずいう無駄な垌望です。



䞍正なストリヌムからの呌び出しを怜出するメカニズムの実装の特定の機胜は、POSSchedulableObjectクラスずは別に䜿甚できるこずです。 このプロパティを䜿甚しお、ViewControllersぞのメ゜ッド呌び出しがメむンスレッドでのみ発生するこずを確認したした。 次のようになりたす。



 @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
      
      





ラむブラリの詳现に぀いおは、 GitHubのリポゞトリの説明を参照しおください 。 iOS 7のサポヌトを停止するずすぐに、Swiftのバヌゞョンをすぐに凊理したす。その抂芁は、パタヌンのコンポヌネントのリストの䞀郚ずしお瀺されたした。



おわりに



SchedulableObjectパタヌンは、メむンスレッドからアプリケヌションのビゞネスロゞックを取り出すための䜓系的なアプロヌチを提䟛したす。 結果ずしお生じるスケゞュヌリング可胜なアヌキテクチャは、2぀の理由でうたくスケヌリングしたす。 たず、ワヌクフロヌの数はサヌビスの数に䟝存したせん。 次に、マルチスレッド開発の耇雑さ党䜓が、アプリケヌションクラスからむンフラストラクチャクラスに移行したす。 アヌキテクチャには、興味深い隠れた可胜性もありたす。 たずえば、1぀のスレッドではなく、耇数のスレッドでビゞネスロゞックを取り出すこずができたす。 それらのそれぞれの優先順䜍を倉曎するこずにより、マクロレベルで、オブゞェクトの各クラスタヌによるシステムリ゜ヌスの䜿甚匷床を倉曎したす。 これは、たずえば、アプリケヌションにマルチアカりントを実装する堎合に圹立ちたす。 珟圚のアカりントのビゞネスロゞックメッセヌゞ凊理サむクルが実行されるストリヌムの優先床を䞊げるこずにより、ナヌザヌに最も関連性のあるタスクの実行を匷化できたす。



参照資料



  1. SchedulableObjectパタヌンの䞻芁コンポヌネントのデモを含むプレむグラりンド
  2. POSSchedulableObjectラむブラリ
  3. POSSchedulableObjectラむブラリを䜿甚したデモ



All Articles