web-dev-qa-db-ja.com

結合フレームワークは非同期操作をシリアル化します

Combineフレームワークを構成する非同期パイプラインを同期的に(シリアルに)整列させるにはどうすればよいですか?

対応するリソースをダウンロードするURLが50個あるとします。一度に1つずつダウンロードするとします。 Operation/OperationQueueでそれを行う方法を知っています。ダウンロードが完了するまで、自分自身の終了を宣言しないOperationサブクラスを使用します。 Combineを使用して同じことをするにはどうすればよいですか?

現時点で発生しているのは、残りのURLのグローバルリストを保持して1つポップし、1つのダウンロードに対して1つのパイプラインを設定し、ダウンロードを実行し、パイプラインのsinkで繰り返します。 。これはCombineのようには見えません。

私はURLの配列を作成し、それをパブリッシャーの配列にマッピングしようとしました。パブリッシャーを "作成"し、flatMapを使用してパイプラインをダウンしてパブリッシュさせることができることはわかっています。しかし、それでも私はまだすべてのダウンロードを同時に行っています。制御された方法でアレイをウォークするCombineの方法はありません—またはありますか?

(Futureで何かをすることも想像していましたが、どうしようもなく混乱しました。この考え方には慣れていません。)

8
matt

これは、可能なアプローチを表す1ページのプレイグラウンドコードです。主なアイデアは、非同期API呼び出しをFutureパブリッシャーのチェーンに変換し、シリアルパイプラインを作成することです。

入力:1から10までのintの範囲。バックグラウンドキューで非同期に文字列に変換されます

非同期APIへの直接呼び出しのデモ:

let group = DispatchGroup()
inputValues.map {
    group.enter()
    asyncCall(input: $0) { (output, _) in
        print(">> \(output), in \(Thread.current)")
        group.leave()
    }
}
group.wait()

出力:

>> 1, in <NSThread: 0x7fe76264fff0>{number = 4, name = (null)}
>> 3, in <NSThread: 0x7fe762446b90>{number = 3, name = (null)}
>> 5, in <NSThread: 0x7fe7624461f0>{number = 5, name = (null)}
>> 6, in <NSThread: 0x7fe762461ce0>{number = 6, name = (null)}
>> 10, in <NSThread: 0x7fe76246a7b0>{number = 7, name = (null)}
>> 4, in <NSThread: 0x7fe764c37d30>{number = 8, name = (null)}
>> 7, in <NSThread: 0x7fe764c37cb0>{number = 9, name = (null)}
>> 8, in <NSThread: 0x7fe76246b540>{number = 10, name = (null)}
>> 9, in <NSThread: 0x7fe7625164b0>{number = 11, name = (null)}
>> 2, in <NSThread: 0x7fe764c37f50>{number = 12, name = (null)}

結合パイプラインのデモ:

出力:

>> got 1
>> got 2
>> got 3
>> got 4
>> got 5
>> got 6
>> got 7
>> got 8
>> got 9
>> got 10
>>>> finished with true

コード:

import Cocoa
import Combine
import PlaygroundSupport

// Assuming there is some Asynchronous API with
// (eg. process Int input value during some time and generates String result)
func asyncCall(input: Int, completion: @escaping (String, Error?) -> Void) {
    DispatchQueue.global(qos: .background).async {
            sleep(.random(in: 1...5)) // wait for random Async API output
            completion("\(input)", nil)
        }
}

// There are some input values to be processed serially
let inputValues = Array(1...10)

// Prepare one pipeline item based on Future, which trasform Async -> Sync
func makeFuture(input: Int) -> AnyPublisher<Bool, Error> {
    Future<String, Error> { promise in
        asyncCall(input: input) { (value, error) in
            if let error = error {
                promise(.failure(error))
            } else {
                promise(.success(value))
            }
        }
    }
    .receive(on: DispatchQueue.main)
    .map {
        print(">> got \($0)") // << sideeffect of pipeline item
        return true
    }
    .eraseToAnyPublisher()
}

// Create pipeline trasnforming input values into chain of Future publishers
var subscribers = Set<AnyCancellable>()
let pipeline =
    inputValues
    .reduce(nil as AnyPublisher<Bool, Error>?) { (chain, value) in
        if let chain = chain {
            return chain.flatMap { _ in
                makeFuture(input: value)
            }.eraseToAnyPublisher()
        } else {
            return makeFuture(input: value)
        }
    }

// Execute pipeline
pipeline?
    .sink(receiveCompletion: { _ in
        // << do something on completion if needed
    }) { output in
        print(">>>> finished with \(output)")
    }
    .store(in: &subscribers)

PlaygroundPage.current.needsIndefiniteExecution = true
1
Asperi

flatMap(maxPublishers:transform:).max(1)とともに使用します。

func imagesPublisher(for urls: [URL]) -> AnyPublisher<UIImage, URLError> {
    Publishers.Sequence(sequence: urls.map { self.imagePublisher(for: $0) })
        .flatMap(maxPublishers: .max(1)) { $0 }
        .eraseToAnyPublisher()
}

どこ

func imagePublisher(for url: URL) -> AnyPublisher<UIImage, URLError> {
    URLSession.shared.dataTaskPublisher(for: url)
        .compactMap { UIImage(data: $0.data) }
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()
}

そして

var imageRequests: AnyCancellable?

func fetchImages() {
    imageRequests = imagesPublisher(for: urls).sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("done")
        case .failure(let error):
            print("failed", error)
        }
    }, receiveValue: { image in
        // do whatever you want with the images as they come in
    })
}

その結果:

serial

ただし、そのように連続して実行することで、パフォーマンスに大きな影響を与えることを認識しておく必要があります。たとえば、一度に6つまで上げると、2倍以上速くなります。

concurrent

個人的には、絶対に必要な場合のみ連続してダウンロードすることをお勧めします(一連の画像/ファイルをダウンロードする場合、ほとんどの場合そうではありません)。はい、リクエストを同時に実行すると、リクエストが特定の順序で終了しない可能性がありますが、順序に依存しない構造体(たとえば、単純な配列ではなくディクショナリ)を使用するだけですが、パフォーマンスの向上は非常に大きいため、一般的に価値があります。

しかし、それらを順番にダウンロードしたい場合は、maxPublishersパラメーターでそれを実現できます。

0
Rob