J'écris du code de réseau dans Swift qui empêche de lancer un téléchargement déjà en cours. Je fais cela en gardant le suivi de l'identité de la demande réseau avec les gestionnaires d'achèvement associés dans un tableau (synchronisé) A. Lorsqu'un appel réseau se termine, il appelle les gestionnaires d'achèvement qui sont associés à cette ressource et il supprime ensuite ces gestionnaires du tableau A.

Je veux m'assurer qu'il n'y a aucun moyen pour les threads d'accéder au tableau dans certains cas. Par exemple, considérez le scénario suivant:

  1. Une demande de téléchargement de la ressource X est lancée.
  2. Vérifiez si la demande a déjà été effectuée.
  3. Ajoutez le gestionnaire de complétion au tableau A.
  4. Si la demande n'a pas été faite, lancez le téléchargement.

Que faire si la ressource X était déjà en cours de téléchargement et que le gestionnaire d'achèvement de ce téléchargement interrompt le thread entre les étapes 2 et 3? Il a été vérifié que la demande a été faite afin que le téléchargement ne démarre pas, mais le nouveau gestionnaire de complétion sera ajouté au tableau A qui ne sera plus jamais appelé.

Comment empêcherais-je cela de se produire? Puis-je verrouiller la baie pour l'écriture pendant que je fais les étapes 2 et 3?

2
Joris Weimar 23 nov. 2017 à 17:53

3 réponses

Meilleure réponse

Je travaille sur l'hypothèse que vous souhaitez pouvoir ajouter plusieurs rappels qui seront tous exécutés lorsque la dernière demande sera terminée, qu'elle soit déjà en cours ou non.

Voici une esquisse d'une solution. Le point de base est de prendre un verrou avant de toucher le (s) tableau (s) de gestionnaires, que ce soit pour en ajouter un ou pour les invoquer une fois la requête terminée. Vous devez également synchroniser la détermination du démarrage ou non d'une nouvelle demande, avec exactement le même verrou .

Si le verrou est déjà maintenu dans la méthode publique où les gestionnaires sont ajoutés et que la propre exécution de la requête s'exécute, alors cette dernière doit attendre la première et vous aurez un comportement déterministe (le nouveau gestionnaire sera appelé).

class WhateverRequester
{
    typealias SuccessHandler = (Whatever) -> Void
    typealias FailureHandler = (Error) -> Void

    private var successHandlers: [SuccessHandler] = []
    private var failureHandlers: [FailureHandler] = []

    private let mutex = // Your favorite locking mechanism here.

    /** Flag indicating whether there's something in flight */
    private var isIdle: Bool = true

    func requestWhatever(succeed: @escaping SuccessHandler,
                         fail: @escaping FailureHandler)
    {
        self.mutex.lock()
        defer { self.mutex.unlock() }

        self.successHandlers.append(succeed)
        self.failureHandlers.append(fail)

        // Nothing to do, unlock and wait for request to finish
        guard self.isIdle else { return }

        self.isIdle = false
        self.enqueueRequest()
    }

    private func enqueueRequest()
    {
        // Make a request however you do, with callbacks to the methods below
    }

    private func requestDidSucceed(whatever: Whatever)
    {
        // Synchronize again before touching the list of handlers and the flag
        self.mutex.lock()
        defer { self.mutex.unlock() }

        for handler in self.successHandlers {
            handler(whatever)
        }

        self.successHandlers = []
        self.failureHandlers = []
        self.isIdle = true
    }

    private func requestDidFail(error: Error)
    {
        // As the "did succeed" method, but call failure handlers
        // Again, lock before touching the arrays and idle flag.
    }
} 

Ceci est si largement applicable que vous pouvez en fait extraire le stockage, le verrouillage et l'appel de rappel dans son propre composant générique, qu'un type "Requester" peut créer, posséder et utiliser.

1
jscs 23 nov. 2017 à 15:39

Sur la base de la réponse de Josh, j'ai créé une requête et un demandeur générique ci-dessous. Il avait quelques besoins plus spécifiques que ceux que j'ai décrits dans la question ci-dessus. Je veux que l'instance Request ne gère que les demandes avec un certain ID (que j'ai transformé en chaîne pour le moment, mais je suppose que cela pourrait également rendre plus générique). Des ID différents nécessitent une instance de requête différente. J'ai créé la classe Requester à cet effet.

La classe de demandeur gère un tableau de requêtes. Par exemple, on pourrait choisir T = UIImage et ID = URL de l'image. Cela nous donnerait un téléchargeur d'image. Ou on pourrait choisir T = User et ID = user id. Cela n'obtiendrait un objet utilisateur qu'une seule fois, même s'il était demandé plusieurs fois.

Je voulais également pouvoir annuler les demandes des appelants individuels. Il marque le gestionnaire d'achèvement avec un ID unique qui est renvoyé à l'appelant. Il peut l'utiliser pour annuler la demande. Si tous les appelants annulent, la demande est supprimée du demandeur.

( Le code ci-dessous n'a pas été testé, je ne peux donc pas garantir qu'il soit exempt de bogues. Utilisez-le à vos propres risques. )

import Foundation

typealias RequestWork<T> = (Request<T>) -> ()
typealias RequestCompletionHandler<T> = (Result<T>) -> ()
typealias RequestCompletedCallback<T> = (Request<T>) -> ()

struct UniqueID {
    private static var ID: Int = 0
    static func getID() -> Int {
        ID = ID + 1
        return ID
    }
}

enum RequestError: Error {
    case canceled
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

protocol CancelableOperation: class {
    func cancel()
}

final class Request<T> {
    private lazy var completionHandlers = [(invokerID: Int,
                                            completion: RequestCompletionHandler<T>)]()
    private let mutex = NSLock()
    // To inform requester the request has finished
    private let completedCallback: RequestCompletedCallback<T>!
    private var isIdle = true
    // After work is executed, operation should be set so the request can be
    // canceled if possible
    var operation: CancelableOperation?
    let ID: String!

    init(ID: String,
         completedCallback: @escaping RequestCompletedCallback<T>) {
        self.ID = ID
        self.completedCallback = completedCallback
    }

    // Cancel the request for a single invoker and it invokes the competion
    // handler with a cancel error. If the only remaining invoker cancels, the
    // request will attempt to cancel
    // the associated operation.
    func cancel(invokerID: Int) {
        self.mutex.lock()
        defer { self.mutex.unlock() }
        if let index = self.completionHandlers.index(where: { $0.invokerID == invokerID }) {
            self.completionHandlers[index].completion(Result.failure(RequestError.canceled))
            self.completionHandlers.remove(at: index)
            if self.completionHandlers.isEmpty {
                self.isIdle = true
                operation?.cancel()
                self.completedCallback(self)
            }
        }
    }

    // Request work to be done. It will only be done if it hasn't been done yet.
    // The work block should set the operation on this request if possible. The
    // work block should call requestFinished(result:) if the work has finished.
    func request(work: @escaping RequestWork<T>,
                 completion: @escaping RequestCompletionHandler<T>) -> Int {
        self.mutex.lock()
        defer { self.mutex.unlock() }
        let ID = UniqueID.getID()
        self.completionHandlers.append((invokerID: ID, completion: completion))
        guard self.isIdle else { return ID }
        work(self)
        self.isIdle = false
        return ID
    }

    // This method should be called from the work block when the work has
    // completed. It will pass the result to all completion handlers and call
    // the Requester class to inform that this request has finished.
    func requestFinished(result: Result<T>) {
        self.mutex.lock()
        defer { self.mutex.unlock() }
        completionHandlers.forEach { $0.completion(result) }
        completionHandlers = []
        self.completedCallback(self)
        self.isIdle = true
    }
}

final class Requester<T>  {
    private lazy var requests = [Request<T>]()
    private let mutex = NSLock()

    init() { }

    // reuqestFinished(request:) should be called after a single Request has
    // finished its work. It removes the requests from the array of requests.
    func requestFinished(request: Request<T>) {
        self.mutex.lock()
        defer { self.mutex.unlock() }
        if let index = requests.index(where: { $0.ID == request.ID }) {
            requests.remove(at: index)
        }
    }

    // request(ID:, work:) will create a request or add a completion handler to
    // an existing request if a request with the supplied ID already exists.
    // When a request is created, it passes a closure that removes the request.
    // It returns the invoker ID to the invoker for cancelation purposes.
    func request(ID: String,
                 work: @escaping RequestWork<T>,
                 completion: @escaping RequestCompletionHandler<T>) ->
        (Int, Request<T>) {
        self.mutex.lock()
        defer { self.mutex.unlock() }
        if let existingRequest = requests.first(where: { $0.ID == ID }) {
            let invokerID = existingRequest.request(work: work, completion: completion)
            return (invokerID, existingRequest)
        } else {
            let request = Request<T>(ID: ID) { [weak self] (request) in
                self?.requestFinished(request: request)
            }
            let invokerID = request.request(work: work, completion: completion)
            return (invokerID, request)
        }
    }
}
1
Joris Weimar 4 déc. 2017 à 07:41

La solution simple est de tout exécuter sur le thread principal sauf le téléchargement proprement dit. Tout ce que vous avez à faire est de faire du gestionnaire d'achèvement un stub qui place un bloc sur la file d'attente principale pour faire tout le travail.

Le pseudo code pour ce que vous voulez est quelque chose comme

assert(Thread.current == Thread.main)
handlerArray.append(myHandler)
if !requestAlreadyRunning)
{
    requestAlreadyRunning = true
    startDownloadRequest(completionHandelr: {
        whatever in
        Dispatch.main.async // This is the only line of code that does not run on the main thread
        {
            for handler in handlerArray
            { 
                handler()
            }
            handlerArray = []
            requestAlreadyRunning = false
        }
    })
}

Cela fonctionne car tout le travail qui peut entraîner des conditions de concurrence et des conflits de synchronisation s'exécute sur un thread - le thread principal et donc le gestionnaire d'achèvement ne peut pas être en cours d'exécution lorsque vous ajoutez de nouveaux gestionnaires d'achèvement à la file d'attente et vice versa.

Notez que, pour que la solution ci-dessus fonctionne, votre application doit être dans une boucle d'exécution. Cela sera vrai pour toute application basée sur Cocoa sur Mac OS ou iOS, mais pas nécessairement pour un outil de ligne de commande. Si tel est le cas ou si vous ne souhaitez pas que le travail se produise sur le thread principal, configurez une file d'attente série et exécutez l'initiation de connexion et le gestionnaire d'achèvement sur celle-ci au lieu de la file d'attente principale.

2
JeremyP 23 nov. 2017 à 16:12
47458249