web-dev-qa-db-fra.com

Combine framework sérialise les opérations asynchrones

Comment faire en sorte que les pipelines asynchrones qui constituent le framework Combine s'alignent de manière synchrone (en série)?

Supposons que je dispose de 50 URL à partir desquelles je souhaite télécharger les ressources correspondantes, et disons que je veux le faire une à la fois. Je sais comment faire cela avec Operation/OperationQueue, par exemple en utilisant une sous-classe Operation qui ne se déclare pas terminée tant que le téléchargement n'est pas terminé. Comment pourrais-je faire la même chose avec Combine?

Pour le moment, tout ce qui me vient à l'esprit est de conserver une liste globale des URL restantes et d'en ouvrir une, de configurer ce pipeline pour un téléchargement, de faire le téléchargement et dans le sink du pipeline, répétez . Cela ne semble pas très semblable à un Combine.

J'ai essayé de créer un tableau des URL et de le mapper à un éventail d'éditeurs. Je sais que je peux "produire" un éditeur et le faire publier sur le pipeline en utilisant flatMap. Mais ensuite, je fais toujours tous les téléchargements simultanément. Il n'y a aucun moyen de combiner pour parcourir le tableau de manière contrôlée - ou y en a-t-il?

(J'ai aussi imaginé faire quelque chose avec Future mais je suis devenu désespérément confus. Je ne suis pas habitué à cette façon de penser.)

8
matt

Voici un code de terrain de jeu d'une page qui décrit une approche possible. L'idée principale est de transformer les appels d'API asynchrones en une chaîne d'éditeurs Future, créant ainsi un pipeline série.

Entrée: plage de int de 1 à 10 qui asynchrone sur la file d'attente d'arrière-plan convertie en chaînes

Démo de l'appel direct à l'API asynchrone:

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

Production:

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

Démo de combiner le pipeline:

Production:

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

Code:

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

Utilisez flatMap(maxPublishers:transform:) avec .max(1), par ex.

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

et

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

Cela a abouti à:

serial

Mais nous devons reconnaître que vous prenez un gros coup de performance en les faisant séquentiellement, comme ça. Par exemple, si je passe à 6 à la fois, c'est plus de deux fois plus rapide:

concurrent

Personnellement, je recommanderais de ne télécharger séquentiellement que si vous le devez absolument (ce qui, lors du téléchargement d’une série d’images/fichiers, n’est certainement pas le cas). Oui, l'exécution simultanée des requêtes peut les empêcher de se terminer dans un ordre particulier, mais nous utilisons simplement une structure indépendante de l'ordre (par exemple, un dictionnaire plutôt qu'un simple tableau), mais les gains de performances sont si importants que cela en vaut généralement la peine.

Mais, si vous voulez qu'ils soient téléchargés séquentiellement, le paramètre maxPublishers peut y parvenir.

0
Rob