) } .branch { stem in stem.chain(loadWebResource =<< 'dataprofile.txt') + stem.chain(loadWebResource =<< 'imagedata.dat') } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion(

HoneyBeeを使用したSwiftの高度な同時実行性

での並行アルゴリズムの設計、テスト、および保守 迅速 は難しく、詳細を正しく把握することは、アプリの成功にとって非常に重要です。並行アルゴリズム(並列プログラミングとも呼ばれます)は、複数の(おそらく多くの)操作を同時に実行して、より多くのハードウェアリソースを活用し、全体的な実行時間を短縮するように設計されたアルゴリズムです。

Appleのプラットフォームでは、並行アルゴリズムを作成する従来の方法は次のとおりです。 NSOperation 。 NSOperationの設計により、プログラマーは、並行アルゴリズムを個々の長時間実行される非同期タスクに細分化することができます。各タスクはNSOperationの独自のサブクラスで定義され、それらのクラスのインスタンスは、実行時にタスクの半順序を作成するために目的のAPIを介して結合されます。並行アルゴリズムを設計するこの方法は、7年間Appleのプラットフォームの最先端でした。

2014年にAppleは導入しました グランドセントラルディスパッチ (GCD)並行操作の表現における劇的な前進として。 GCDは、それに付随して電力を供給する新しい言語機能ブロックとともに、非同期要求の開始直後に非同期応答ハンドラーをコンパクトに記述する方法を提供しました。プログラマーは、並行タスクの定義を多数のNSOperationサブクラスの複数のファイルに分散することを推奨されなくなりました。これで、同時アルゴリズム全体を1つのメソッド内で実行可能に記述できるようになりました。この表現力と型安全性の向上は、概念の大きな前進でした。この書き方の典型的なアルゴリズムは、次のようになります。



func processImageData(completion: (result: Image?, error: Error?) -> Void) { loadWebResource('dataprofile.txt') { (dataResource, error) in guard let dataResource = dataResource else { completion(nil, error) return } loadWebResource('imagedata.dat') { (imageResource, error) in guard let imageResource = imageResource else { completion(nil, error) return } decodeImage(dataResource, imageResource) { (imageTmp, error) in guard let imageTmp = imageTmp else { completion(nil, error) return } dewarpAndCleanupImage(imageTmp) { imageResult in guard let imageResult = imageResult else { completion(nil, error) return } completion(imageResult, nil) } } } } }

このアルゴリズムを少し分解してみましょう。関数processImageDataは、独自の4つの非同期呼び出しを行って作業を完了する非同期関数です。 4つの非同期呼び出しは、ブロックベースの非同期処理に最も自然な方法で相互にネストされます。結果ブロックにはそれぞれオプションのErrorパラメーターがあり、1つを除くすべてに、aysnc操作の結果を示す追加のオプションパラメーターが含まれています。

上記のコードブロックの形は、おそらくほとんどのSwift開発者に馴染みがあるように見えます。しかし、このアプローチの何が問題になっていますか?次の問題点のリストは、おそらく同じようによく知られています。

どうすればもっとうまくできるでしょうか? ミツバチ Swift並行プログラミングを簡単、表現力豊か、そして安全にするfutures / promisesライブラリです。上記の非同期アルゴリズムをHoneyBeeで書き直して、結果を調べてみましょう。

func processImageData(completion: (result: Image?, error: Error?) -> Void) { HoneyBee.start() .setErrorHandler { completion(nil, $0) } .branch { stem in stem.chain(loadWebResource =<< 'dataprofile.txt') + stem.chain(loadWebResource =<< 'imagedata.dat') } .chain(decodeImage) .chain(dewarpAndCleanupImage) .chain { completion($0, nil) } }

この実装が開始する最初の行は、新しいHoneyBeeレシピです。 2行目は、デフォルトのエラーハンドラーを確立します。 HoneyBeeレシピでは、エラー処理はオプションではありません。何かがうまくいかない場合は、アルゴリズムがそれを処理する必要があります。 3行目は、並列実行を可能にするブランチを開きます。 loadWebResourceの2つのチェーンは並行して実行され、それらの結果が結合されます(5行目)。ロードされた2つのリソースの合計値は、decodeImageに転送されます。完了が呼び出されるまで、チェーンの下流に続きます。

上記の問題点のリストを見ていき、HoneyBeeがこのコードをどのように改善したかを見てみましょう。この機能の保守が大幅に簡単になりました。 HoneyBeeレシピは、それが表現するアルゴリズムのように見えます。コードは読みやすく、理解しやすく、すばやく変更できます。 HoneyBeeの設計により、命令の順序を間違えると、実行時エラーではなく、コンパイル時エラーが発生します。この関数は、バグや人為的エラーの影響を受けにくくなりました。

発生する可能性のあるすべてのランタイムエラーは完全に処理されています。 HoneyBeeがサポートするすべての関数シグネチャ(38個あります)は、完全に処理されることが保証されています。この例では、Objective-Cスタイルの2パラメーターコールバックは、エラーハンドラーにルーティングされるnil以外のエラーを生成するか、チェーンを下に進むnil以外の値を生成するか、または両方の場合値がnilであるHoneyBeeは、関数コールバックがそのコントラクトを実行していないことを説明するエラーを生成します。

HoneyBeeは、関数のコールバックが呼び出される回数の契約上の正確さも処理します。関数がコールバックの呼び出しに失敗した場合、HoneyBeeは記述的な失敗を生成します。関数がコールバックを複数回呼び出す場合、HoneyBeeは補助的な呼び出しを抑制し、警告をログに記録します。これらの障害応答(およびその他)は両方とも、プログラマーの個々のニーズに合わせてカスタマイズできます。

うまくいけば、この形式のprocessImageDataがすでに明らかであるはずです。リソースのダウンロードを適切に並列化して、最適なパフォーマンスを提供します。 HoneyBeeの最も強力な設計目標の1つは、レシピがそれが表現するアルゴリズムのように見えることです。

ずっといい。正しい?しかし、HoneyBeeにはさらに多くの機能があります。

警告:次のケーススタディは、気の弱い人向けではありません。次の問題の説明を検討してください。モバイルアプリがCoreDataを使用しているその状態を維持します。 NSManagedObjectがありますメディアと呼ばれるモデル。これは、バックエンドサーバーにアップロードされたメディアアセットを表します。ユーザーは、一度に数十のメディアアイテムを選択し、それらをバッチでバックエンドシステムにアップロードすることができます。メディアは最初に参照文字列を介して表されます。参照文字列はMediaオブジェクトに変換する必要があります。幸い、アプリにはすでにそれを行うヘルパーメソッドが含まれています。

func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

メディア参照がMediaオブジェクトに変換されたら、メディアアイテムをバックエンドにアップロードする必要があります。ここでも、ネットワーク関連の処理を実行する準備ができたヘルパー関数があります。

func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

ユーザーは一度に数十のメディアアイテムを選択できるため、UXデザイナーはアップロードの進行状況についてかなり強力なフィードバックを指定しています。要件は、次の4つの機能にまとめられています。

/// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

ただし、アプリは期限切れになることがあるメディア参照をソースしているため、アップロードの少なくとも半分が成功した場合、ビジネスマネージャーはユーザーに「成功」​​メッセージを送信することを決定しました。つまり、試行されたアップロードの半分未満が失敗した場合、並行プロセスは勝利を宣言し、totalProcessSuccessを呼び出す必要があります。これは、開発者として渡された仕様です。しかし、経験豊富なプログラマーとして、適用しなければならない要件がもっとあることに気づきます。

もちろん、Businessはバッチアップロードをできるだけ早く実行することを望んでいるため、シリアルアップロードは問題外です。アップロードは並行して実行する必要があります。

しかしあまりありません。無差別にasyncバッチ全体で、数十の同時アップロードがモバイルNIC(ネットワークインターフェイスカード)にフラッディングし、アップロードは実際にはシリアルよりも遅く、速くは進みません。

モバイルネットワーク接続は安定しているとは見なされません。短いトランザクションでも、ネットワーク接続の変更のみが原因で失敗する可能性があります。アップロードが失敗したことを本当に宣言するには、アップロードを少なくとも1回再試行する必要があります。

再試行ポリシーには、一時的な障害が発生しないため、エクスポート操作を含めないでください。

エクスポートプロセスは計算に依存するため、メインスレッドから実行する必要があります。

エクスポートはコンピューティングバウンドであるため、プロセッサのスラッシングを回避するために、残りのアップロードプロセスよりも同時インスタンスの数を少なくする必要があります。

上記の4つのコールバック関数はすべてUIを更新するため、すべてメインスレッドで呼び出す必要があります。

メディアはNSManagedObjectであり、NSManagedObjectContextに由来します。尊重しなければならない独自のスレッド要件があります。

この問題の仕様は少しあいまいに見えますか?このような問題が将来潜んでいることに気付いても驚かないでください。私は自分の仕事でこのようなものに遭遇しました。まず、従来のツールを使用してこの問題を解決してみましょう。バックルアップ、これはきれいではありません。

/// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts

うわー!コメントなしで、それは約75行です。あなたはずっと推論に従いましたか?新しい仕事で最初の週にこのモンスターに遭遇した場合、どのように感じますか?それを維持する準備ができていると思いますか、それとも変更しますか?エラーが含まれているかどうかわかりますか?エラーが含まれていますか?

ここで、HoneyBeeの代替案を検討してください。

HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

このフォームはどのようにあなたを襲いますか?少しずつ見ていきましょう。最初の行で、メインスレッドから始めてHoneyBeeレシピを開始します。メインスレッドから開始することで、すべてのエラーがメインスレッドのerrorHandler(2行目)に渡されるようにします。 3行目はmediaReferencesを挿入しますプロセスチェーンへの配列。次に、並列処理に備えて、グローバルバックグラウンドキューに切り替えます。 5行目で、mediaReferencesのそれぞれに対して並列反復を開始します。この並列処理は、最大4つの同時操作に制限されます。また、サブチェーンの少なくとも半分が成功した場合(エラーを起こさないでください)、完全な反復が成功したと見なされることを宣言します。 6行目はfinallyを宣言しています以下のサブチェーンが成功するか失敗するかに関係なく呼び出されるリンク。 finallyについてリンク、メインスレッド(7行目)に切り替えてsingleUploadCompletionを呼び出します(8行目)。 10行目では、エクスポート操作(11行目)の周囲で最大並列化を1(単一実行)に設定しています。 13行目は、managedObjectContextが所有するプライベートキューに切り替わります。インスタンス。 14行目は、アップロード操作の1回の再試行を宣言しています(15行目)。 17行目は再びメインスレッドに切り替わり、18行目はsingleUploadSuccessを呼び出します。ライン20が実行されるまでに、すべての並列反復が完了します。失敗した反復の半分未満の場合、行20は最後にもう一度メインキューに切り替わり(それぞれがバックグラウンドキューで実行されたことを思い出してください)、21はインバウンド値をドロップし(まだmediaReferences)、22は|を呼び出します。 _ + _ |。

HoneyBeeフォームは、保守が簡単なことは言うまでもなく、より明確で、よりクリーンで、読みやすくなっています。 Mediaオブジェクトをmap関数のような配列に再統合するためにループが必要な場合、このアルゴリズムの長い形式はどうなりますか?変更を加えた後、アルゴリズムのすべての要件がまだ満たされていることをどの程度確信できますか? HoneyBeeフォームでは、この変更は、並列マップ機能を使用するためにそれぞれをマップに置き換えることです。 (はい、それも減少しています。)

HoneyBeeは、非同期および並行アルゴリズムの記述をより簡単、安全、表現力豊かにするSwiftの強力な先物ライブラリです。この記事では、HoneyBeeを使用して、アルゴリズムの保守をより簡単に、より正確に、より高速にする方法について説明しました。 HoneyBeeは、再試行サポート、複数のエラーハンドラー、リソースガード、コレクション処理(非同期形式のmap、filter、reduce)などの他の主要な非同期パラダイムもサポートしています。機能の完全なリストについては、を参照してください。 ウェブサイト 。詳細や質問については、新品をご覧ください コミュニティフォーラム

付録:非同期関数の契約上の正確性の確保

機能の契約上の正確さを保証することは、コンピュータサイエンスの基本的な信条です。事実上すべての最新のコンパイラが、値を返すことを宣言する関数が1回だけ返されることを確認するためのチェックを持っているほどです。 1回未満または複数回の戻りはエラーとして扱われ、完全なコンパイルを適切に防ぎます。

ただし、このコンパイラ支援は通常、非同期関数には適用されません。次の(遊び心のある)例を考えてみましょう。

totalProcessSuccess

func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } completion('Pistachio') } else if int < 2 { completion('Vanilla') } } 関数はIntを受け入れ、非同期で文字列を返します。明らかな問題がいくつか含まれていても、swiftコンパイラは上記の形式を正しいものとして受け入れます。特定の入力が与えられると、この関数は完了を0回、1回、または2回呼び出す場合があります。非同期関数を使用したことのあるプログラマーは、自分の作業でこの問題の例を思い出すことがよくあります。私たちは何ができる?確かに、コードをリファクタリングしてきれいにすることができます(範囲ケースのあるスイッチはここで機能します)。ただし、機能の複雑さを軽減するのが難しい場合があります。定期的に関数を返す場合と同じように、コンパイラが正確さの検証を支援してくれるとよいのではないでしょうか。

方法があることがわかりました。次の迅速な呪文を観察してください。

generateIcecream

この関数の先頭に挿入された4行は、完了コールバックが1回だけ呼び出されたことをコンパイラーに確認させます。これは、この関数がコンパイルされなくなったことを意味します。どうしたの?最初の行で、この関数に最終的に生成させたい結果を宣言しますが、初期化しません。未定義のままにしておくことで、使用する前に1回割り当てる必要があり、宣言することで、2回割り当てられないようにします。 2行目は、この関数の最後のアクションとして実行される延期です。 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } // else completion('Pistachio') } else if int < 2 { completion('Vanilla') } } で完了ブロックを呼び出します-関数の残りの部分によって割り当てられた後。 3行目では、完了と呼ばれる新しい定数を作成します。この定数は、コールバックパラメーターをシャドウします。新しい補完は、パブリックAPIを宣言しないVoidタイプです。この行は、この行の後に補完を使用するとコンパイラエラーになることを保証します。 2行目の延期は、完了ブロックの唯一の許可された使用法です。 4行目では、新しい完了定数が使用されていない場合に発生するコンパイラ警告を削除しています。

そのため、この非同期関数が契約を履行していないことを迅速なコンパイラに報告させることに成功しました。正しくするための手順を見ていきましょう。まず、コールバックへのすべての直接アクセスをfinalResultへの割り当てに置き換えましょう。

finalResult

現在、コンパイラは2つの問題を報告しています。

func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } // else finalResult = 'Pistachio' } else if int < 2 { finalResult = 'Vanilla' } }

予想通り、この関数にはerror: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = 'Pistachio' という経路があります。ゼロ回割り当てられ、複数回割り当てられる経路も割り当てられます。これらの問題は次のように解決します。

finalResult

「ピスタチオ」は適切なelse節に移動され、一般的なケース(もちろん「ナポリタン」)をカバーできなかったことがわかりました。

今説明したパターンは、オプションの値、オプションのエラー、または一般的な結果列挙型のような複雑なタイプを返すように簡単に調整できます。コールバックが1回だけ呼び出されることを確認するようにコンパイラーを強制することにより、非同期関数の正確性と完全性を主張できます。

基本を理解する

プログラミングにおける並行性とは何ですか?

並行アルゴリズム(並列プログラミングとも呼ばれます)は、複数の(おそらく多くの)操作を同時に実行して、より多くのハードウェアリソースを活用し、全体的な実行時間を短縮するように設計されたアルゴリズムです。

並行性の問題は何ですか?

ネストされたコードブロックの「破滅のピラミッド」形状はすぐに扱いにくくなり、非同期エラー処理は直感的でないか不完全になる可能性があり、サードパーティの統合に関する問題は悪化し、パフォーマンスの向上を目的としていますが、無駄と最適ではない可能性がありますパフォーマンス。

, nil) } }

この実装が開始する最初の行は、新しいHoneyBeeレシピです。 2行目は、デフォルトのエラーハンドラーを確立します。 HoneyBeeレシピでは、エラー処理はオプションではありません。何かがうまくいかない場合は、アルゴリズムがそれを処理する必要があります。 3行目は、並列実行を可能にするブランチを開きます。 loadWebResourceの2つのチェーンは並行して実行され、それらの結果が結合されます(5行目)。ロードされた2つのリソースの合計値は、decodeImageに転送されます。完了が呼び出されるまで、チェーンの下流に続きます。

上記の問題点のリストを見ていき、HoneyBeeがこのコードをどのように改善したかを見てみましょう。この機能の保守が大幅に簡単になりました。 HoneyBeeレシピは、それが表現するアルゴリズムのように見えます。コードは読みやすく、理解しやすく、すばやく変更できます。 HoneyBeeの設計により、命令の順序を間違えると、実行時エラーではなく、コンパイル時エラーが発生します。この関数は、バグや人為的エラーの影響を受けにくくなりました。

発生する可能性のあるすべてのランタイムエラーは完全に処理されています。 HoneyBeeがサポートするすべての関数シグネチャ(38個あります)は、完全に処理されることが保証されています。この例では、Objective-Cスタイルの2パラメーターコールバックは、エラーハンドラーにルーティングされるnil以外のエラーを生成するか、チェーンを下に進むnil以外の値を生成するか、または両方の場合値がnilであるHoneyBeeは、関数コールバックがそのコントラクトを実行していないことを説明するエラーを生成します。

HoneyBeeは、関数のコールバックが呼び出される回数の契約上の正確さも処理します。関数がコールバックの呼び出しに失敗した場合、HoneyBeeは記述的な失敗を生成します。関数がコールバックを複数回呼び出す場合、HoneyBeeは補助的な呼び出しを抑制し、警告をログに記録します。これらの障害応答(およびその他)は両方とも、プログラマーの個々のニーズに合わせてカスタマイズできます。

うまくいけば、この形式のprocessImageDataがすでに明らかであるはずです。リソースのダウンロードを適切に並列化して、最適なパフォーマンスを提供します。 HoneyBeeの最も強力な設計目標の1つは、レシピがそれが表現するアルゴリズムのように見えることです。

ギリシャの債務危機の原因

ずっといい。正しい?しかし、HoneyBeeにはさらに多くの機能があります。

警告:次のケーススタディは、気の弱い人向けではありません。次の問題の説明を検討してください。モバイルアプリがCoreDataを使用しているその状態を維持します。 NSManagedObjectがありますメディアと呼ばれるモデル。これは、バックエンドサーバーにアップロードされたメディアアセットを表します。ユーザーは、一度に数十のメディアアイテムを選択し、それらをバッチでバックエンドシステムにアップロードすることができます。メディアは最初に参照文字列を介して表されます。参照文字列はMediaオブジェクトに変換する必要があります。幸い、アプリにはすでにそれを行うヘルパーメソッドが含まれています。

func export(_ mediaRef: String, completion: @escaping (Media?, Error?) -> Void) { // transcoding stuff completion(Media(context: managedObjectContext), nil) }

メディア参照がMediaオブジェクトに変換されたら、メディアアイテムをバックエンドにアップロードする必要があります。ここでも、ネットワーク関連の処理を実行する準備ができたヘルパー関数があります。

func upload(_ media: Media, completion: @escaping (Error?) -> Void) { // network stuff completion(nil) }

ユーザーは一度に数十のメディアアイテムを選択できるため、UXデザイナーはアップロードの進行状況についてかなり強力なフィードバックを指定しています。要件は、次の4つの機能にまとめられています。

/// Called if anything goes wrong in the upload func errorHandler(_ error: Error) { // do the right thing } /// Called once per mediaRef, after either a successful or unsuccessful upload func singleUploadCompletion(_ mediaRef: String) { // update a progress indicator } /// Called once per successful upload func singleUploadSuccess(_ media: Media) { // do celebratory things } /// Called if the entire batch was considered to be uploaded successfully. func totalProcessSuccess() { // declare victory }

ただし、アプリは期限切れになることがあるメディア参照をソースしているため、アップロードの少なくとも半分が成功した場合、ビジネスマネージャーはユーザーに「成功」​​メッセージを送信することを決定しました。つまり、試行されたアップロードの半分未満が失敗した場合、並行プロセスは勝利を宣言し、totalProcessSuccessを呼び出す必要があります。これは、開発者として渡された仕様です。しかし、経験豊富なプログラマーとして、適用しなければならない要件がもっとあることに気づきます。

もちろん、Businessはバッチアップロードをできるだけ早く実行することを望んでいるため、シリアルアップロードは問題外です。アップロードは並行して実行する必要があります。

しかしあまりありません。無差別にasyncバッチ全体で、数十の同時アップロードがモバイルNIC(ネットワークインターフェイスカード)にフラッディングし、アップロードは実際にはシリアルよりも遅く、速くは進みません。

モバイルネットワーク接続は安定しているとは見なされません。短いトランザクションでも、ネットワーク接続の変更のみが原因で失敗する可能性があります。アップロードが失敗したことを本当に宣言するには、アップロードを少なくとも1回再試行する必要があります。

再試行ポリシーには、一時的な障害が発生しないため、エクスポート操作を含めないでください。

エクスポートプロセスは計算に依存するため、メインスレッドから実行する必要があります。

エクスポートはコンピューティングバウンドであるため、プロセッサのスラッシングを回避するために、残りのアップロードプロセスよりも同時インスタンスの数を少なくする必要があります。

モバイルウェブデザインのベストプラクティス

上記の4つのコールバック関数はすべてUIを更新するため、すべてメインスレッドで呼び出す必要があります。

メディアはNSManagedObjectであり、NSManagedObjectContextに由来します。尊重しなければならない独自のスレッド要件があります。

この問題の仕様は少しあいまいに見えますか?このような問題が将来潜んでいることに気付いても驚かないでください。私は自分の仕事でこのようなものに遭遇しました。まず、従来のツールを使用してこの問題を解決してみましょう。バックルアップ、これはきれいではありません。

/// An enum describing specific problems that the algorithm might encounter. enum UploadingError : Error { case invalidResponse case tooManyFailures } /// A semaphore to prevent flooding the NIC let outerLimit = DispatchSemaphore(value: 4) /// A semaphore to prevent thrashing the processor let exportLimit = DispatchSemaphore(value: 1) /// The number of times to retry the upload if it fails let uploadRetries = 1 /// Dispatch group to keep track of when the entire process is finished let fullProcessDispatchGroup = DispatchGroup() /// How many of the uploads fully completed. var uploadSuccesses = 0 // this notify block is called when the full process has completed. fullProcessDispatchGroup.notify(queue: DispatchQueue.main) { let successRate = Float(uploadSuccesses) / Float(mediaReferences.count) if successRate > 0.5 { totalProcessSuccess() } else { errorHandler(UploadingError.tooManyFailures) } } // start in the background DispatchQueue.global().async { for mediaRef in mediaReferences { // alert the group that we're starting a process fullProcessDispatchGroup.enter() // wait until it's safe to start uploading outerLimit.wait() /// common cleanup operations needed later func finalizeMediaRef() { singleUploadCompletion(mediaRef) fullProcessDispatchGroup.leave() outerLimit.signal() } // wait until it's safe to start exporting exportLimit.wait() export(mediaRef) { (media, error) in // allow another export to begin exportLimit.signal() if let error = error { DispatchQueue.main.async { errorHandler(error) finalizeMediaRef() } } else { guard let media = media else { DispatchQueue.main.async { errorHandler(UploadingError.invalidResponse) finalizeMediaRef() } return } // the export was successful var uploadAttempts = 0 /// define the upload process and its retry behavior func doUpload() { // respect Media's threading requirements managedObjectContext.perform { upload(media) { error in if let error = error { if uploadAttempts

うわー!コメントなしで、それは約75行です。あなたはずっと推論に従いましたか?新しい仕事で最初の週にこのモンスターに遭遇した場合、どのように感じますか?それを維持する準備ができていると思いますか、それとも変更しますか?エラーが含まれているかどうかわかりますか?エラーが含まれていますか?

デザインアートの5つの原則

ここで、HoneyBeeの代替案を検討してください。

HoneyBee.start(on: DispatchQueue.main) .setErrorHandler(errorHandler) .insert(mediaReferences) .setBlockPerformer(DispatchQueue.global()) .each(limit: 4, acceptableFailure: .ratio(0.5)) { elem in elem.finally { link in link.setBlockPerformer(DispatchQueue.main) .chain(singleUploadCompletion) } .limit(1) { link in link.chain(export) } .setBlockPerformer(managedObjectContext) .retry(1) { link in link.chain(upload) // subject to transient failure } .setBlockPerformer(DispatchQueue.main) .chain(singleUploadSuccess) } .setBlockPerformer(DispatchQueue.main) .drop() .chain(totalProcessSuccess)

このフォームはどのようにあなたを襲いますか?少しずつ見ていきましょう。最初の行で、メインスレッドから始めてHoneyBeeレシピを開始します。メインスレッドから開始することで、すべてのエラーがメインスレッドのerrorHandler(2行目)に渡されるようにします。 3行目はmediaReferencesを挿入しますプロセスチェーンへの配列。次に、並列処理に備えて、グローバルバックグラウンドキューに切り替えます。 5行目で、mediaReferencesのそれぞれに対して並列反復を開始します。この並列処理は、最大4つの同時操作に制限されます。また、サブチェーンの少なくとも半分が成功した場合(エラーを起こさないでください)、完全な反復が成功したと見なされることを宣言します。 6行目はfinallyを宣言しています以下のサブチェーンが成功するか失敗するかに関係なく呼び出されるリンク。 finallyについてリンク、メインスレッド(7行目)に切り替えてsingleUploadCompletionを呼び出します(8行目)。 10行目では、エクスポート操作(11行目)の周囲で最大並列化を1(単一実行)に設定しています。 13行目は、managedObjectContextが所有するプライベートキューに切り替わります。インスタンス。 14行目は、アップロード操作の1回の再試行を宣言しています(15行目)。 17行目は再びメインスレッドに切り替わり、18行目はsingleUploadSuccessを呼び出します。ライン20が実行されるまでに、すべての並列反復が完了します。失敗した反復の半分未満の場合、行20は最後にもう一度メインキューに切り替わり(それぞれがバックグラウンドキューで実行されたことを思い出してください)、21はインバウンド値をドロップし(まだmediaReferences)、22は|を呼び出します。 _ + _ |。

HoneyBeeフォームは、保守が簡単なことは言うまでもなく、より明確で、よりクリーンで、読みやすくなっています。 Mediaオブジェクトをmap関数のような配列に再統合するためにループが必要な場合、このアルゴリズムの長い形式はどうなりますか?変更を加えた後、アルゴリズムのすべての要件がまだ満たされていることをどの程度確信できますか? HoneyBeeフォームでは、この変更は、並列マップ機能を使用するためにそれぞれをマップに置き換えることです。 (はい、それも減少しています。)

HoneyBeeは、非同期および並行アルゴリズムの記述をより簡単、安全、表現力豊かにするSwiftの強力な先物ライブラリです。この記事では、HoneyBeeを使用して、アルゴリズムの保守をより簡単に、より正確に、より高速にする方法について説明しました。 HoneyBeeは、再試行サポート、複数のエラーハンドラー、リソースガード、コレクション処理(非同期形式のmap、filter、reduce)などの他の主要な非同期パラダイムもサポートしています。機能の完全なリストについては、を参照してください。 ウェブサイト 。詳細や質問については、新品をご覧ください コミュニティフォーラム

付録:非同期関数の契約上の正確性の確保

機能の契約上の正確さを保証することは、コンピュータサイエンスの基本的な信条です。事実上すべての最新のコンパイラが、値を返すことを宣言する関数が1回だけ返されることを確認するためのチェックを持っているほどです。 1回未満または複数回の戻りはエラーとして扱われ、完全なコンパイルを適切に防ぎます。

ただし、このコンパイラ支援は通常、非同期関数には適用されません。次の(遊び心のある)例を考えてみましょう。

totalProcessSuccess

func generateIcecream(from int: Int, completion: (String) -> Void) { if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } completion('Pistachio') } else if int < 2 { completion('Vanilla') } } 関数はIntを受け入れ、非同期で文字列を返します。明らかな問題がいくつか含まれていても、swiftコンパイラは上記の形式を正しいものとして受け入れます。特定の入力が与えられると、この関数は完了を0回、1回、または2回呼び出す場合があります。非同期関数を使用したことのあるプログラマーは、自分の作業でこの問題の例を思い出すことがよくあります。私たちは何ができる?確かに、コードをリファクタリングしてきれいにすることができます(範囲ケースのあるスイッチはここで機能します)。ただし、機能の複雑さを軽減するのが難しい場合があります。定期的に関数を返す場合と同じように、コンパイラが正確さの検証を支援してくれるとよいのではないでしょうか。

方法があることがわかりました。次の迅速な呪文を観察してください。

generateIcecream

この関数の先頭に挿入された4行は、完了コールバックが1回だけ呼び出されたことをコンパイラーに確認させます。これは、この関数がコンパイルされなくなったことを意味します。どうしたの?最初の行で、この関数に最終的に生成させたい結果を宣言しますが、初期化しません。未定義のままにしておくことで、使用する前に1回割り当てる必要があり、宣言することで、2回割り当てられないようにします。 2行目は、この関数の最後のアクションとして実行される延期です。 func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { completion('Chocolate') } else if int < 10 { completion('Strawberry') } // else completion('Pistachio') } else if int < 2 { completion('Vanilla') } } で完了ブロックを呼び出します-関数の残りの部分によって割り当てられた後。 3行目では、完了と呼ばれる新しい定数を作成します。この定数は、コールバックパラメーターをシャドウします。新しい補完は、パブリックAPIを宣言しないVoidタイプです。この行は、この行の後に補完を使用するとコンパイラエラーになることを保証します。 2行目の延期は、完了ブロックの唯一の許可された使用法です。 4行目では、新しい完了定数が使用されていない場合に発生するコンパイラ警告を削除しています。

そのため、この非同期関数が契約を履行していないことを迅速なコンパイラに報告させることに成功しました。正しくするための手順を見ていきましょう。まず、コールバックへのすべての直接アクセスをfinalResultへの割り当てに置き換えましょう。

finalResult

現在、コンパイラは2つの問題を報告しています。

Webおよびモバイルアプリケーションの開発
func generateIcecream(from int: Int, completion: (String) -> Void) { let finalResult: String defer { completion(finalResult) } let completion: Void = Void() defer { completion } if int > 5 { if int <20 { finalResult = 'Chocolate' } else if int < 10 { finalResult = 'Strawberry' } // else finalResult = 'Pistachio' } else if int < 2 { finalResult = 'Vanilla' } }

予想通り、この関数にはerror: AsyncCorrectness.playground:1:8: error: constant 'finalResult' used before being initialized defer { completion(finalResult) } ^ error: AsyncCorrectness.playground:11:3: error: immutable value 'finalResult' may only be initialized once finalResult = 'Pistachio' という経路があります。ゼロ回割り当てられ、複数回割り当てられる経路も割り当てられます。これらの問題は次のように解決します。

finalResult

「ピスタチオ」は適切なelse節に移動され、一般的なケース(もちろん「ナポリタン」)をカバーできなかったことがわかりました。

今説明したパターンは、オプションの値、オプションのエラー、または一般的な結果列挙型のような複雑なタイプを返すように簡単に調整できます。コールバックが1回だけ呼び出されることを確認するようにコンパイラーを強制することにより、非同期関数の正確性と完全性を主張できます。

基本を理解する

プログラミングにおける並行性とは何ですか?

並行アルゴリズム(並列プログラミングとも呼ばれます)は、複数の(おそらく多くの)操作を同時に実行して、より多くのハードウェアリソースを活用し、全体的な実行時間を短縮するように設計されたアルゴリズムです。

並行性の問題は何ですか?

ネストされたコードブロックの「破滅のピラミッド」形状はすぐに扱いにくくなり、非同期エラー処理は直感的でないか不完全になる可能性があり、サードパーティの統合に関する問題は悪化し、パフォーマンスの向上を目的としていますが、無駄と最適ではない可能性がありますパフォーマンス。