web-dev-qa-db-fra.com

Essayer de comprendre la sous-classe de fonctionnement asynchrone

J'essaie de commencer à utiliser Operation dans un projet parallèle plutôt que d'avoir des rappels basés sur la fermeture jonchés dans tout mon code réseau pour aider à éliminer les appels imbriqués. Je faisais donc quelques lectures sur le sujet, et je suis tombé sur l'implémentation this :

open class AsynchronousOperation: Operation {

    // MARK: - Properties

    private let stateQueue = DispatchQueue(label: "asynchronous.operation.state", attributes: .concurrent)

    private var rawState = OperationState.ready

    private dynamic var state: OperationState {
        get {
            return stateQueue.sync(execute: {
                rawState
            })
        }
        set {
            willChangeValue(forKey: "state")
            stateQueue.sync(flags: .barrier, execute: {
                rawState = newValue
            })
            didChangeValue(forKey: "state")
        }
    }

    public final override var isReady: Bool {
        return state == .ready && super.isReady
    }

    public final override var isExecuting: Bool {
        return state == .executing
    }

    public final override var isFinished: Bool {
        return state == .finished
    }

    public final override var isAsynchronous: Bool {
        return true
    }


    // MARK: - NSObject

    private dynamic class func keyPathsForValuesAffectingIsReady() -> Set<String> {
        return ["state"]
    }

    private dynamic class func keyPathsForValuesAffectingIsExecuting() -> Set<String> {
        return ["state"]
    }

    private dynamic class func keyPathsForValuesAffectingIsFinished() -> Set<String> {
        return ["state"]
    }


    // MARK: - Foundation.Operation

    public final override func start() {
        super.start()

        if isCancelled {
            finish()
            return
        }

        state = .executing
        execute()
    }


    // MARK: - Public

    /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.
    open func execute() {
        fatalError("Subclasses must implement `execute`.")
    }

    /// Call this function after any work is done or after a call to `cancel()` to move the operation into a completed state.
    public final func finish() {
        state = .finished
    }
}

@objc private enum OperationState: Int {

    case ready

    case executing

    case finished
}

Il y a quelques détails d'implémentation de cette sous-classe Operation que j'aimerais avoir de l'aide pour comprendre.

  1. À quoi sert la propriété stateQueue? Je vois qu'il est utilisé par get et set de la propriété calculée state, mais je ne trouve aucune documentation expliquant les sync:flags:execute Et sync:execute Méthodes qu'ils utilisent.

  2. À quoi servent les trois méthodes de classe de la section NSObject qui renvoient ["state"]? Je ne les vois utilisés nulle part. J'ai trouvé, dans NSObject, class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String>, mais cela ne semble pas m'aider à comprendre pourquoi ces méthodes sont déclarées.

22
Nick Kohrn

Tu as dit:

  1. À quoi sert la propriété stateQueue? Je vois qu'il est utilisé par get et set de la propriété calculée state, mais je ne trouve aucune documentation expliquant les méthodes sync:flags:execute Et sync:execute Qu'ils utilisent.

Ce code "synchronise" l'accès à une propriété pour la rendre sûre pour les threads. Pour savoir pourquoi vous devez le faire, consultez la documentation Operation , qui conseille:

Considérations multicœurs

... Lorsque vous sous-classe NSOperation, vous devez vous assurer que toutes les méthodes remplacées restent sûres d'appeler à partir de plusieurs threads. Si vous implémentez des méthodes personnalisées dans votre sous-classe, telles que des accesseurs de données personnalisés, vous devez également vous assurer que ces méthodes sont thread-safe. Ainsi, l'accès à toutes les variables de données de l'opération doit être synchronisé pour éviter toute corruption potentielle des données. Pour plus d'informations sur la synchronisation, consultez Guide de programmation des threads .

Concernant l'utilisation exacte de cette file d'attente simultanée pour la synchronisation, il s'agit du modèle "lecteur-écrivain". Ce concept de base du modèle lecteur-écrivain est que les lectures peuvent se produire simultanément les unes par rapport aux autres (d'où sync, sans barrière), mais les écritures ne doivent jamais être effectuées simultanément en ce qui concerne tout autre accès à cette propriété ( d'où async avec barrière). Tout cela est décrit dans la vidéo WWDC 2012 Modèles de conception asynchrones avec blocs, GCD et XPC . Notez que, bien que cette vidéo présente le concept de base, elle utilise l'ancienne syntaxe dispatch_sync Et dispatch_barrier_async, Plutôt que la Swift 3 et la syntaxe ultérieure de seulement sync et async(flags: .barrier) syntaxe utilisée ici.

Vous avez également demandé:

  1. Quel est le but des trois méthodes de classe dans la section NSObject qui renvoient ["state"]? Je ne les vois utilisés nulle part. J'ai trouvé, dans NSObject, class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String>, mais cela ne semble pas m'aider à comprendre pourquoi ces méthodes sont déclarées.

Ce ne sont que des méthodes qui garantissent que les modifications apportées à la propriété state déclenchent KVN pour les propriétés isReady , isExecuting et - isFinished . Le KVN de ces trois clés est essentiel pour le bon fonctionnement des opérations asynchrones. Quoi qu'il en soit, cette syntaxe est décrite dans le Key-Value Observing Programming Guide: Registering Dependent Keys .

La méthode keyPathsForValuesAffectingValue que vous avez trouvée est liée. Vous pouvez soit enregistrer des clés dépendantes à l'aide de cette méthode, soit avoir les méthodes individuelles comme indiqué dans votre extrait de code d'origine.


BTW, voici une version révisée de la classe AsynchronousOperation que vous avez fournie, à savoir:

  1. Vous ne devez pas appeler super.start(). Comme le dit la documentation start (soulignement ajouté):

    Si vous implémentez une opération simultanée, vous devez remplacer cette méthode et l'utiliser pour lancer votre opération. Votre implémentation personnalisée ne doit à aucun moment appeler super.

  2. Ajoutez @objc Requis dans Swift 4.

  3. Renommé execute pour utiliser main, qui est la convention pour les sous-classes Operation.

  4. Il est inapproprié de déclarer isReady en tant que propriété final. Toute sous-classe devrait avoir le droit d'affiner davantage sa logique isReady (bien que nous le reconnaissions rarement).

  5. Utilisez #keyPath Pour rendre le code un peu plus sûr/robuste.

  6. Vous n'avez pas besoin de faire KVN manuel lors de l'utilisation de la propriété dynamic. L'appel manuel de willChangeValue et didChangeValue n'est pas nécessaire dans cet exemple.

  7. Modifiez finish pour qu'il ne passe à l'état .finished Que s'il n'est pas déjà terminé.

Donc:

public class AsynchronousOperation: Operation {

    /// State for this operation.

    @objc private enum OperationState: Int {
        case ready
        case executing
        case finished
    }

    /// Concurrent queue for synchronizing access to `state`.

    private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)

    /// Private backing stored property for `state`.

    private var _state: OperationState = .ready

    /// The state of the operation

    @objc private dynamic var state: OperationState {
        get { return stateQueue.sync { _state } }
        set { stateQueue.async(flags: .barrier) { self._state = newValue } }
    }

    // MARK: - Various `Operation` properties

    open         override var isReady:        Bool { return state == .ready && super.isReady }
    public final override var isExecuting:    Bool { return state == .executing }
    public final override var isFinished:     Bool { return state == .finished }

    // KVN for dependent properties

    open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if ["isReady", "isFinished", "isExecuting"].contains(key) {
            return [#keyPath(state)]
        }

        return super.keyPathsForValuesAffectingValue(forKey: key)
    }

    // Start

    public final override func start() {
        if isCancelled {
            state = .finished
            return
        }

        state = .executing

        main()
    }

    /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.

    open override func main() {
        fatalError("Subclasses must implement `main`.")
    }

    /// Call this function to finish an operation that is currently executing

    public final func finish() {
        if !isFinished { state = .finished }
    }
}
37
Rob

Lorsque vous utilisez un extrait de code mis à jour de réponse de Rob , il faut être conscient de la possibilité d'un bogue, provoqué par ce changement:

  1. Modifiez la finition pour qu'elle ne passe à l'état .finished que si isExecuting.

Ce qui précède va à l'encontre de Apple docs :

En plus de simplement quitter lorsqu'une opération est annulée, il est également important de déplacer une opération annulée vers l'état final approprié. Plus précisément, si vous gérez vous-même les valeurs des propriétés finies et en cours d'exécution (peut-être parce que vous implémentez une opération simultanée), vous devez mettre à jour ces propriétés en conséquence. Plus précisément, vous devez modifier la valeur renvoyée par terminé à YES et la valeur renvoyée en exécutant à NO. Vous devez effectuer ces modifications même si l'opération a été annulée avant de commencer son exécution.

Cela entraînera un bogue dans quelques cas. Par exemple, si la file d'attente d'opérations avec "maxConcurrentOperationCount = 1" obtient 3 opérations asynchrones A B et C, alors si toutes les opérations sont annulées pendant A, C ne sera pas exécuté et la file d'attente sera bloquée sur l'opération B.

3
Roman Kozak

À propos de votre première question: stateQueue verrouille votre opération lors de l'écriture d'une nouvelle valeur dans votre état de fonctionnement en:

    return stateQueue.sync(execute: {
            rawState
    })

Et

    stateQueue.sync(flags: .barrier, execute: {
        rawState = newValue
    })

comme votre opération est asynchrone, donc avant de lire ou d'écrire un état, un autre état peut être appelé. Comme vous voulez écrire isExecution mais en attendant, isFinished est déjà appelé. Ainsi, pour éviter ce scénario, stateQueue verrouille l'état de l'opération à lire et à écrire jusqu'à ce qu'il ait terminé son appel précédent. Son travail comme Atomic. Utilisez plutôt la file d'attente de répartition, vous pouvez utiliser une extension de NSLock pour simplifier l'exécution de code critique à partir de l'exemple de code Advanced NSOperations dans WWDC 2015 https://developer.Apple.com/videos/play/wwdc2015/226/ from https://developer.Apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.Zip et vous pouvez implémenter comme suit:

private let stateLock = NSLock()

private dynamic var state: OperationState {
    get {
        return stateLock.withCriticalScope{ rawState } 
    }
    set {
        willChangeValue(forKey: "state")

        stateLock.withCriticalScope { 
            rawState = newValue
        }
        didChangeValue(forKey: "state")
    }
}

À propos de votre deuxième question: Il s'agit d'une notification KVO pour la propriété en lecture seule isReady, isExecuting, isFinished pour gérer l'état de fonctionnement. Vous pouvez lire ceci: http://nshipster.com/key-value-observing postez jusqu'à la fin pour une meilleure compréhension de KVO.

1
Evana