📖

【翻訳】Concurrency in Swift (Custom Operations Part 4)

2023/07/18に公開

図1に示すように、2つのオペレーションを作成し、両方をオペレーション・キューに追加した。これに加えて、オペレーション1の終了後にオペレーション2を開始するという依存ルールを追加した。以前のブログで紹介したように、操作キューは自動的に別のスレッドで操作を実行する。

オペレーション・キューにオペレーションを追加すると、キューは非同期プロパティの値を無視して、常に別のスレッドからstartメソッドを呼び出します。したがって、常にオペレーション・キューに追加してオペレーションを実行するのであれば、それを非同期にする理由はありません。


図1

図2に示すように、依存性ルールを追加したにもかかわらず、タスク2が開始されました。その理由は、タスク1が別のキューでタスクをディスパッチしてすぐに戻り、操作キューはタスク2が終了したと判断したためです。実際のアプリケーションでは、別のキューでタスクを実行するNSUrlSessionを使用しているため、この問題に何度も直面しました。


図2

この問題は、操作の状態を手動で変更することで解決できる。NSOperationクラスは、操作の実行状態を自動的に追跡するための基本的なロジックを提供しますが、私たちのタスクは別のキューでディスパッチしているので、操作キューはタスクが実際にいつ終了したかを知る必要があります。

オペレーションの状態

オペレーションは、ディスパッチ・グループよりも複雑なライフサイクルを持つ。これにより、より大きな制御が可能になります。Operationオブジェクトは、いつ実行すれば安全かを判断するために、また、外部クライアントにOperationのライフサイクルの進行を通知するために、内部でステート情報を保持します。カスタム・サブクラスはこのステート情報を保持し、コード内でオペレーションが正しく実行されるようにします。オペレーションのステートに関連する主要なパスは次のとおりです。

isReady →isReadyキーパスは、オペレーションの実行準備が整ったことをクライアントに知らせる。readyプロパティには、そのオペレーションが今すぐ実行できる場合はtrue、そのオペレーションに依存している未完了のオペレーションがまだある場合はfalseという値が格納されます。ほとんどの場合、このキーパスの状態を自分で管理する必要はありません。しかし、オペレーションの準備完了が依存するオペレーション以外の要因で決定される場合(プログラム内の外部条件など)、readyプロパティの独自の実装を用意し、操作の準備完了を自分で追跡することができます。しかし、多くの場合、外部状態が許可したときだけオペレーション・オブジェクトを作成する方が簡単です。

isExecuting →isExecutingキー・パスは、オペレーションが割り当てられたタスクに積極的に取り組んでいるかどうかをクライアントに知らせます。executingプロパティは、オペレーションがタスクに取り組んでいる場合はtrueを、取り組んでいない場合はfalseを報告しなければなりません。

isFinished → isFinished キーパスは、オペレーションがそのタスクを正常に終了したか、キャンセルされて終了したかをクライアントに知らせます。オペレーションオブジェクトは、isFinished キーパスの値が true に変わるまで、依存関係をクリアしません。同様に、オペレーションキューは、finished プロパティに値 true が含まれるまで、オペレーションをキューから削除しません。このように、操作を完了とマークすることは、キューが進行中の操作やキャンセルされた操作でバックアップされないようにするために非常に重要です。

isCancelled →isCancelledキーパスは、オペレーションのキャンセルが要求されたことをクライアントに知らせます。キャンセルのサポートは任意であるが、推奨されるものであり、あなた自身のコードがこのキーパスに対してKVO通知を送る必要はないはずです。

概要

操作が開始されると、その状態はisReadyからisExecutingへと進行する。画像のダウンロードのような非同期タスクの場合は、ネットワークにコールを送ってすぐに戻ります。終了したように見えます。現在のスレッドではもはや何もしていませんが、非同期タスクはバックグラウンド・スレッドで実行されています。本当に終了するまで、手動で操作の状態をisExecutingに設定する方法が必要です。

操作状態のプロパティは読み取り専用です。直接設定することはできません。では、どのように非同期操作の値を管理するのでしょうか。オペレーション・ステート・プロパティが正しい値を返すように何かをしなければなりません。オペレーション・クラスは、状態の通知を送信するために、KVO(key value observation)に依存しています。

非同期操作の作成

図3に示すように、再利用可能な非同期オペレーションを作成した。以下の点を考慮する必要がある。

  1. まず、非同期オペレーションの状態を保持する状態列挙型を作成します。その代わりに、
          非同期オペレーション用のステート・プロパティを作成し、このプロパティを使ってベース・クラスの
    オペレーション・ステート・プロパティを管理します。非同期オペレーションには、
      その状態を管理するための変数プロパティが必要です。

  2. 非同期操作のデフォルトの初期状態値が準備できました。ベース・クラスのステート・プロパティを
    オーバーライドして、新しいステート・プロパティを使うことができる。Appleのドキュメントによると、
    isReadyをオーバーライドしないでください。ほとんどの場合、このisReadyキーパスの状態を自分で
    管理する必要はありません。しかし、操作のレディネスが依存する操作以外の要素によって決定される場合
    (プログラム内の外部条件など)、readyプロパティの独自の実装を用意して、操作のレディネスを自分で
    追跡することができます。しかし、外部状態が許可したときだけオペレーション・オブジェクトを作成する方が
    単純な場合が多い。

  3. 新しい状態を使用するには、操作の状態プロパティisExecutingとisFinishedを設定するだけです。

  4. 非同期オペレーション・オブジェクトを実装する場合は、isAsynchronousプロパティを実装して
          trueを返す必要があります。このプロパティは、オペレーション・キューの外でオペレーションを
    手動で実行する場合にのみ使用します。このプロパティは、メイン・スレッドからオペレーションが
    実行されることを保証します。

  5. そのため、非同期オペレーションは、そのステートの値が変更されるたびに
    KVO通知を送信する必要があります。ステート・プロパティにプロパティ・オブザーバを追加し、
    ベース・クラスにwillChangeValueとdidChangeValueを呼び出してステートを更新する。
    willSet()では、現在の状態と次の状態のプロパティが変更されることをオペレーション・キューに
    通知する。そして、didSetでは、前の状態と新しい状態のプロパティが変更されたことを
    操作キューに通知する。

  6. KVOのミッションは達成された。

  7. 非同期操作は、Prefixを持つ基底クラスのプロパティに対して
    KVO通知をトリガーしなければならないので、計算プロパティkeypathを作成した。

  8. 次に、オペレーションのstartメソッドとcancelメソッドをオーバーライドする必要がある。startメソッドは、
    オペレーションがキャンセルされたかどうかをチェックし、非同期操作の状態をfinishedに設定する。
    オペレーションがキャンセルされていない場合は、main 関数メンバが呼び出され、
    これはすぐに戻る非同期操作なので、手動で状態を executing に設定する必要があります。

  9. startのオーバーライドからsuper.start()を呼び出さないでください。
    startのドキュメントにあるように、並行処理を実装する場合は、このメソッドをオーバーライドして、
    処理を開始するために使用する必要があります。
    カスタムの実装では、super.start()を呼び出してはいけません。

図4に示すように、特定の非同期操作では、mainをオーバーライドしてfinishedを呼び出すだけで
大丈夫です。これで、別のスレッドで実行されているタスクに関係なく、オペレーション2が終了した後に
オペレーション2が開始されることが分かります。これは、カスタム/サブクラス・オペレーションを
使用するユースケースの1つです。これに加えて、オペレーション・キューにオペレーションを追加したので、
オペレーション・キューは通常通り別のスレッドを使ってオペレーションのメイン・メソッドを実行します。


図4

図5に示すように、操作キューは1つのスレッドを使用します。これらの操作は依存関係にあるため、
これらのタスクに1つのスレッドを使用することができます。スレッドの参照はスワップできるが、
スレッドの共有プールから1つのスレッドにしか問い合わせないことに注意。


図5

図6に示すように、依存関係を取り除いた後、2つのスレッドを要求しているのがわかる。
これがどれほど賢いか、想像がつくでしょう。


図6

スレッドセーフ

前の例で示したように、1つのバグとして、stateがスレッドセーフでないことが考えられます。
stateを設定すると、スレッド・サニタイザーの問題が発生する可能性があります。
また、Applyは、オーバーライドされたプロパティもスレッドセーフな方法でステータスを
提供しなければならないことを示唆している。図7に示すように、stateプロパティをアトミックな方法にします。
バリアが何かわからない場合は、前編を参照してください。


図7

図8.1と図8.2に示すように、私たちはキューをオペレーションキューに提供し、私たちのProvidedOperationQueueCustomQueueがオペレーションキューによって
タスクのディスパッチに使用されていることがわかります。

underlyingQueue → オペレーションの実行に使用されるディスパッチ・キュー。
このプロパティの値を既存のディスパッチ・キューに設定することで、
そのディスパッチ・キューに投入されたブロックの中に、
キューイングされたオペレーションを紛れ込ませることができます。
このプロパティの値は、キューにオペレーションが存在しない場合にのみ設定されるべきです。operationCountが0に等しくない場合にこのプロパティの値を設定すると、
invalidArgumentExceptionが発生します。
このプロパティの値は、 dispatch_get_main_queue() が返す値であってはならない。
基本となるディスパッチキューに設定されたサービス品質レベルは、
オペレーションキューの qualityOfService プロパティに設定された値を上書きします。


図8.1


図8.2

図9に示すように、操作回数が0でない場合(つまり、オペレーションキューに操作を追加した後)に
underlyingQueueプロパティの値を設定すると、invalidArgumentExceptionが発生します。


図9

有益なリンク

https://www.avanderlee.com/swift/asynchronous-operations/

https://www.kodeco.com/9461083-ios-concurrency-with-gcd-and-operations/lessons/28

https://blog.bitbebop.com/asynchronous-operations-swift/

https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/

【翻訳元の記事】

Concurrency in Swift (Custom Operations Part 4)
https://ali-akhtar.medium.com/concurrency-in-swift-custom-operations-part-4-154b60bff84c

Discussion