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.
À 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.
À 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.
Tu as dit:
- À quoi sert la propriété
stateQueue
? Je vois qu'il est utilisé par get et set de la propriété calculéestate
, mais je ne trouve aucune documentation expliquant les méthodessync:flags:execute
Etsync: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é:
- 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é, dansNSObject
,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:
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
.
Ajoutez @objc
Requis dans Swift 4.
Renommé execute
pour utiliser main
, qui est la convention pour les sous-classes Operation
.
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).
Utilisez #keyPath
Pour rendre le code un peu plus sûr/robuste.
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.
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 }
}
}
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:
- 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.
À 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.