【翻訳】Concurrency in Swift (Operations and Operation Queue Part 3)

2023/07/18に公開

swiftにおける並行処理シリーズの続きです。基本を理解するには

パート1(https://medium.com/@aliakhtar_16369/concurrency-in-swift-grand-central-dispatch-part-1-945ff05e8863)

パート2(https://medium.com/@aliakhtar_16369/concurrency-in-swift-grand-central-dispatch-part-2-1b0b025ee381)

をご覧ください。

このパートでは、以下のトピックを取り上げます。

  1. オペレーションとは何か?
  2. タスクを非同期で実行するためのブロック、NSInvocationOperation、カスタム・オペレーションの作成
  3. オペレーションをキャンセルする方法
  4. オペレーションキューとは
  5. オペレーションキューにオペレーションを追加する
  6. オペレーション間の依存関係の作り方
  7. オペレーション・キューとGCDの違い
  8. オペレーションキューを使ったディスパッチグループの実装
オペレーション

オペレーションは、非同期に実行したい作業をカプセル化するオブジェクト指向の方法です。
オペレーションは、オペレーション・キューと組み合わせて使うか、
あるいはそれ自体で使うように設計されています。

オペレーション・オブジェクトは、アプリケーションに実行させたい作業をカプセル化するために使用する、OperationまたはNSOperationクラス(Foundationフレームワーク内)のインスタンスです。

Operationクラス自体は抽象基底クラスであり、有用な処理を行うにはサブクラス化する必要があります。
抽象クラスであるにもかかわらず、このクラスは、独自のサブクラスで行わなければならない作業を
最小限に抑えるために、かなりの量のインフラストラクチャーを提供します。
さらに、Foundationフレームワークは、既存のコードでそのまま使用できる2つの具象サブクラスを提供します。

オペレーション・ステート

オペレーションには、そのライフサイクルを表すステートマシンがあります。
このライフサイクルのさまざまな部分で起こりうる状態がいくつかあります。

・ インスタンス化されると、isReady状態に遷移する。

・ startメソッドを呼び出すと、isExecuting状態に遷移する。

・ タスクが終了すると、isFinished状態に遷移する。

・ タスクが進行中にキャンセルを呼び出すと、
isCancelled状態に遷移してからisFinished状態に遷移する。

オペレーションを作成するには、主に以下の3つの方法があるります。

1.BlockOperation (具象クラス)

1つ以上のブロック・オブジェクトを同時に実行するために、そのまま使用するクラスです。
複数のブロックを実行できるため、ブロック操作オブジェクトはグループ・セマンティックを使って動作します。
関連するブロックの実行がすべて終了したときに初めて、操作自体が終了したとみなされます。
ブロック操作では、操作の依存関係、KVO、通知、キャンセルなどを利用できます。

図1に示すように、我々はこのコードを非同期で実行しました。つまり、このコードはすぐに戻ってきますが、
メイン・スレッド上でoperation.start()が呼び出されたため、
メイン・スレッドをブロックしてしまうという悪い知らせがあります。

Operationオブジェクトはデフォルトで同期的に実行されます。
つまり、startメソッドを呼び出したスレッドでタスクを実行します。


図1

同期マナーとは一体何か、1つ以上のブロック・オブジェクトを同時に実行することです。

図1.0.1に示すように、ブロック操作に追加されたタスク/ブロック自体は同時に実行されますが、
ブロックが同期的に実行されるということは、開始が呼び出されたスレッドをブロックするということです。


図1.0.1

図1.0.2に示すように、他のスレッドでstartメソッドを呼び出しているので、そのスレッドはブロックされます。


図1.0.2

図1.0.3に示すように、
すべての同時実行ブロックが実行されたときに呼び出される完了ブロックも追加できます。


図1.0.3

ブロック操作の同時実行

図1.1に示すように、バックグラウンド・スレッドでstart()メソッドを呼び出すと、
そのスレッドでタスクが実行されます。これを実行するには、オペレーションキューを使う方法があります。


図1.1

2.NSInvocationOperation (具象クラス)

アプリケーションのオブジェクトとセレクタに基づいてオペレーションオブジェクトを作成するために、
そのまま使用するクラスです。

Objective CではNSInvocationOperationを作成できますが、Swiftでは利用できません。

3.カスタム・オペレーション

Operationをサブクラス化すると、独自の操作の実装を完全に制御できるようになり、
操作のデフォルトの実行方法やステータスの報告方法などを変更できるようになります。

図2に示すように、Operation基底クラスをサブクラス化し、そのmainメソッドをオーバーライドすることで、
カスタム・オペレーションを作成しました。サブクラス化すると、mainメソッドにタスクを追加することになります。
非同期カスタム・オペレーションを実装し、メイン・スレッドをブロックしました。


図2

オペレーションを手動で実行する予定で、なおかつ非同期で実行させたい場合は、
確実に実行されるように適切な処置を講じる必要があります。
そのためには、オペレーションオブジェクトを並行操作として定義します。

図3に示すように、タスクを並行して実行するために以下のステップを実行しました。

  1. サブクラス MyConcurrentQueue を作成した。
    タイプミス:名前はMyConcurrentOperationsとすべきである。

  2. start()メソッドを呼び出すと、バックグラウンド・スレッドでmain()メソッドが呼び出される。

  3. mainメソッドでタスクを定義し、1つ注意すべき点は、cancelケースも定義していることです。

  4. カスタム操作でcancelを呼び出すと、isCancelled状態に遷移してループが切れ、
    図3に示すように39487件だけが表示されます。


図3

オペレーションキュー

  1. オペレーションキューは、CocoaのGCDの高レベル抽象化です。

  2. オペレーション・キューを使えば、オペレーションの本当の力を見ることができます。
          自分でオペレーションを開始する代わりに、オペレーション・キューにオペレーションを与え、
    それがスケジューリングと実行を処理します。

  3. オペレーションキューは、非同期に実行したい作業をカプセル化するオブジェクト指向の方法です。

  4. オペレーション・キューにオペレーション(タスク/作業)を追加する。

オペレーションの追加

図4に示すように、(Blockを使用して)2つのオペレーションを作成し、オペレーション・キューに追加しました。
オペレーションキューは、バックグラウンドのスレッドで2つのオペレーションを開始し、実行します。
カスタムスレッドでstart()メソッドを呼び出す必要はありません。オペレーションキューにオペレーションを
追加すると、準備ができ次第実行されます。


図4

図5に示すように、タスクをシリアルに実行しています。
つまり、オペレーション・キューを使ってシリアル・キューを実装しています。

maxConcurrentOperationCount →同時に実行できるキューの最大数。デフォルト値は-1。


図5

maxConcurrentOperationCount = 2 に設定することで、
同時実行キューを作成し、図6に示すようにタスクが同時実行されています。


図6

オペレーションの依存関係

図7に示すように、2つのタスクの間に依存関係を追加することで、再びシリアル・キューを作成しました。
2つのブロック・オペレーションを作成し、タスク2が終了するまでタスク1を開始しないように、blockOperations1.addDependency(blockOperations2)を呼び出しています。

オペレーションキューを使ったディスパッチグループの実装

Part2では、GCDディスパッチ・グループの機能を使って、1つ以上のタスクが実行を終えるまでスレッドを
ブロックしました。図8に示すように、依存関係を利用することで、同じ動作をオペレーション・キューを使って
実装しました。これは、指定したタスクがすべて完了するまで処理を進められない場合に非常に便利です。

図8に示すように、3つのタスクがあり、同時に実行したい。すべてのタスクが終了したら、
何らかのメソッドを呼び出して、すべてのタスクが終了したことを示す必要があります。

  1. オペレーション・キューを作成

  2. タスクを実行する3つのブロック・オペレーションを作成

  3. 3つのタスクがすべて終了したときに実行される完了ブロック・オペレーション(blockOperations4)を
    作成した。

  4. blockOperations4をblockOperations1、blockOperations2、blockOperations3に
    依存させる。つまり、3つのタスクがすべて終了したときにblockOperations4が実行されるようにする。

  5. waitUntilFinished → オペレーションオブジェクトのタスクが終了するまで、
    現在のスレッドの実行をブロックする。

  6. このコードを実行すると、task1、task2、task3が終了したときに
    "All Operation is Completed "と表示される。


図8

図9に示すように、waitUntilFinished = trueを設定することで、メインスレッドをブロックするだけです。
では、どのような場合に役に立つのかが問題ですが、次のセクションでその答えが得られるでしょう。


図9

図10に示すように、依存関係を一切使用せずに、オペレーションキューを使用してディスパッチ・グループの
動作を実装しました。バックグラウンド・スレッドであれば、この動作を実現するためにこのスレッドを
ブロックすることができます。このコードを理解するために、DispatchQueue.global().asyncメソッドを使って意図的にバックグラウンド・スレッドに切り替えました。

操作キューにタスク1、タスク2、タスク3を実行させ、これらの同時実行タスクが実行を終えるまで、
現在のスレッドをブロックするように指示しました。


図10

GCDを超えるオペレーション・キューの利点

  1. Operation APIは依存関係をサポートしています。
    タスク間の複雑な依存関係は、GCDでも実現できますが、多くの作業を行う必要があります。

  2. NSOperationクラスとNSOperationQueueクラスは、KVO(Key Value Observing)を使用して、
    観察することができる多くのプロパティを持っています。オペレーションやオペレーション・キューの状態を
    監視したい場合、これも重要な利点です。

  3. オペレーションは一時停止、再開、キャンセルが可能です。グランド・セントラル・ディスパッチ
    (Grand Central Dispatch)を使ってタスクをディスパッチすると、そのタスクの実行を
    コントロールしたり、洞察したりすることはできなくなります。その点、NSOperation APIはより柔軟で、
    オペレーションのライフサイクルを開発者がコントロールできます。

  4. NSOperationQueue には、さらに多くの利点があります。
          例えば、同時に実行できるキューオペレーションの最大数を指定できます。
          これにより、同時に実行する操作の数を簡単に制御したり、
    連続した操作キューを作成したりできます。

有益なリンク

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW1

【翻訳元の記事】

Concurrency in Swift (Operations and Operation Queue Part 3)
https://ali-akhtar.medium.com/concurrency-in-swift-operations-and-operation-queue-part-3-a108fbe27d61

Discussion