Les variables Swift sont-elles atomiques?

102

En Objective-C, vous avez une distinction entre les propriétés atomiques et non atomiques:

@property (nonatomic, strong) NSObject *nonatomicObject;
@property (atomic, strong) NSObject *atomicObject;

D'après ce que je comprends, vous pouvez lire et écrire des propriétés définies comme atomiques à partir de plusieurs threads en toute sécurité, tandis que l'écriture et l'accès à des propriétés non atomiques ou à des ivars à partir de plusieurs threads en même temps peuvent entraîner un comportement non défini, y compris des erreurs d'accès.

Donc, si vous avez une variable comme celle-ci dans Swift:

var object: NSObject

Puis-je lire et écrire dans cette variable en parallèle en toute sécurité? (Sans considérer la signification réelle de cela).

Lassej
la source
Je pense qu'à l'avenir, nous pourrons peut-être utiliser @atomicou @nonatomic. ou juste atomique par défaut. (Swift est tellement incomplet, nous ne pouvons pas en dire beaucoup maintenant)
Bryan Chen
1
OMI, ils rendront tout non atomique par défaut, et fourniront probablement une fonctionnalité spéciale pour créer des objets atomiques.
eonil
En passant, atomicn'est généralement pas considéré comme suffisant pour une interaction thread-safe avec une propriété, sauf pour les types de données simples. Pour les objets, on synchronise généralement l'accès à travers les threads en utilisant des verrous (par exemple, NSLockou @synchronized) ou des files d'attente GCD (par exemple, une file d'attente série ou une file d'attente simultanée avec un modèle "lecteur-écrivain").
Rob
@Rob, vrai, bien qu'en raison du comptage de références dans Objective-C (et éventuellement dans Swift), la lecture et l'écriture simultanées dans une variable sans accès atomique peuvent entraîner une corruption de la mémoire. Si toutes les variables avaient un accès atomique, la pire chose qui puisse arriver serait une condition de concurrence "logique", c'est-à-dire un comportement inattendu.
lassej
Ne vous méprenez pas: j'espère qu'Apple répond / résout la question du comportement atomique. C'est juste que (a) atomicn'assure pas la sécurité des threads pour les objets; et (b) si l'on utilise correctement l'une des techniques de synchronisation susmentionnées pour assurer la sécurité des threads (entre autres, empêcher les lectures / écritures simultanées), le problème atomique est sans objet. Mais nous en avons toujours besoin / le voulons pour les types de données simples, où atomica une valeur réelle. Bonne question!
Rob

Réponses:

52

Il est très tôt pour supposer qu'aucune documentation de bas niveau n'est disponible, mais vous pouvez étudier à partir de l'assemblage. Le désassembleur de trémie est un excellent outil.

@interface ObjectiveCar : NSObject
@property (nonatomic, strong) id engine;
@property (atomic, strong) id driver;
@end

Utilisations objc_storeStronget objc_setProperty_atomicpour non atomique et atomique respectivement, où

class SwiftCar {
    var engine : AnyObject?    
    init() {
    }
}

utilise swift_retainde libswift_stdlib_coreet, apparemment, n'a pas de sécurité des threads intégrée.

Nous pouvons supposer que des mots clés supplémentaires (similaires à @lazy) pourraient être introduits plus tard.

Mise à jour 20/07/15 : selon cet article de blog sur les singletons, l' environnement rapide peut rendre certains cas thread-safe pour vous, à savoir:

class Car {
    static let sharedCar: Car = Car() // will be called inside of dispatch_once
}

private let sharedCar: Car2 = Car2() // same here
class Car2 {

}

Mise à jour 25/05/16 : Gardez un œil sur la proposition d'évolution rapide https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - on dirait que c'est va être possible d'avoir un @atomiccomportement mis en œuvre par vous-même.

Sash Zats
la source
J'ai mis à jour ma réponse avec des informations récentes, j'espère que cela aidera
Sash Zats
1
Hé, merci pour le lien vers l'outil de désassemblage de trémie. Ça a l'air bien.
C0D3
11

Swift n'a pas de construction de langage autour de la sécurité des threads. Il est supposé que vous utiliserez les bibliothèques fournies pour faire votre propre gestion de la sécurité des threads. Il existe un grand nombre d'options pour implémenter la sécurité des threads, y compris les mutex pthread, NSLock et dispatch_sync en tant que mécanisme de mutex. Voir le récent post de Mike Ash sur le sujet: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Donc, la réponse directe à votre question de "Can Je lis et j'écris cette variable en parallèle en toute sécurité? " est non.

Bon Doug
la source
7

Il est probablement trop tôt pour répondre à cette question. Actuellement, swift manque de modificateurs d'accès, il n'y a donc pas de moyen évident d'ajouter du code qui gère la concurrence autour d'un getter / setter de propriétés. De plus, le langage Swift ne semble pas encore avoir d'informations sur la concurrence! (Il manque aussi KVO etc ...)

Je pense que la réponse à cette question deviendra claire dans les prochaines versions.

Coline
la source
re: manque de KVO, vérifiez willSet, didSet- semble être un premier pas sur le chemin
Sash Zats
1
willSet, didSet est plus destiné aux propriétés qui avaient toujours besoin d'un setter personnalisé car elles devaient faire quelque chose. Par exemple, une propriété de couleur qui doit redessiner une vue lorsque la propriété est remplacée par une valeur différente; cela se fait maintenant plus facilement avec didSet.
gnasher729
oui, c'est ce que je voulais dire par "une première étape" :) J'ai supposé que cela pourrait être un signe que la fonctionnalité était disponible mais pas encore pleinement implémentée
Sash Zats
6

Détails

  • Xcode 9.1, Swift 4
  • Xcode 10.2.1 (10E1001), Swift 5

Liens

Types mis en œuvre

Idée principale

class Example {

    private lazy var semaphore = DispatchSemaphore(value: 1)

    func executeThreadSafeFunc1() {
        // Lock access. Only first thread can execute code below.
        // Other threads will wait until semaphore.signal() will execute
        semaphore.wait()
        // your code
        semaphore.signal()         // Unlock access
    }

    func executeThreadSafeFunc2() {
        // Lock access. Only first thread can execute code below.
        // Other threads will wait until semaphore.signal() will execute
        semaphore.wait()
        DispatchQueue.global(qos: .background).async {
            // your code
            self.semaphore.signal()         // Unlock access
        }
    }
}

Exemple d'accès atomique

class Atomic {

    let dispatchGroup = DispatchGroup()
    private var variable = 0

    // Usage of semaphores

    func semaphoreSample() {

        // value: 1 - number of threads that have simultaneous access to the variable
        let atomicSemaphore = DispatchSemaphore(value: 1)
        variable = 0

        runInSeveralQueues { dispatchQueue  in
            // Only (value) queqes can run operations betwen atomicSemaphore.wait() and atomicSemaphore.signal()
            // Others queues await their turn
            atomicSemaphore.wait()            // Lock access until atomicSemaphore.signal()
            self.variable += 1
            print("\(dispatchQueue), value: \(self.variable)")
            atomicSemaphore.signal()          // Unlock access
        }

        notifyWhenDone {
            atomicSemaphore.wait()           // Lock access until atomicSemaphore.signal()
            print("variable = \(self.variable)")
            atomicSemaphore.signal()         // Unlock access
        }
    }

    // Usage of sync of DispatchQueue

    func dispatchQueueSync() {
        let atomicQueue = DispatchQueue(label: "dispatchQueueSync")
        variable = 0

        runInSeveralQueues { dispatchQueue  in

            // Only queqe can run this closure (atomicQueue.sync {...})
            // Others queues await their turn
            atomicQueue.sync {
                self.variable += 1
                print("\(dispatchQueue), value: \(self.variable)")
            }
        }

        notifyWhenDone {
            atomicQueue.sync {
                print("variable = \(self.variable)")
            }
        }
    }

    // Usage of objc_sync_enter/objc_sync_exit

    func objcSync() {
        variable = 0

        runInSeveralQueues { dispatchQueue  in

            // Only one queqe can run operations betwen objc_sync_enter(self) and objc_sync_exit(self)
            // Others queues await their turn
            objc_sync_enter(self)                   // Lock access until objc_sync_exit(self).
            self.variable += 1
            print("\(dispatchQueue), value: \(self.variable)")
            objc_sync_exit(self)                    // Unlock access
        }

        notifyWhenDone {
            objc_sync_enter(self)                   // Lock access until objc_sync_exit(self)
            print("variable = \(self.variable)")
            objc_sync_exit(self)                    // Unlock access
        }
    }
}

// Helpers

extension Atomic {

    fileprivate func notifyWhenDone(closure: @escaping ()->()) {
        dispatchGroup.notify(queue: .global(qos: .utility)) {
            closure()
            print("All work done")
        }
    }

    fileprivate func runInSeveralQueues(closure: @escaping (DispatchQueue)->()) {

        async(dispatch: .main, closure: closure)
        async(dispatch: .global(qos: .userInitiated), closure: closure)
        async(dispatch: .global(qos: .utility), closure: closure)
        async(dispatch: .global(qos: .default), closure: closure)
        async(dispatch: .global(qos: .userInteractive), closure: closure)
    }

    private func async(dispatch: DispatchQueue, closure: @escaping (DispatchQueue)->()) {

        for _ in 0 ..< 100 {
            dispatchGroup.enter()
            dispatch.async {
                let usec = Int(arc4random()) % 100_000
                usleep(useconds_t(usec))
                closure(dispatch)
                self.dispatchGroup.leave()
            }
        }
    }
}

Usage

Atomic().semaphoreSample()
//Atomic().dispatchQueueSync()
//Atomic().objcSync()

Résultat

entrez la description de l'image ici

Vasily Bodnarchuk
la source
Un exemple de projet sur github serait bien!
Klaas
1
salut! Ceci est un échantillon complet. Copiez la Atomicclasse et exécutez-la en utilisantAtomic().semaphoreSample()
Vasily Bodnarchuk
Oui, je l'ai déjà fait. J'ai pensé que ce serait bien de l'avoir en tant que projet mis à jour avec la syntaxe la plus récente. Avec Swift, la syntaxe change tout le temps. Et votre réponse est de loin la plus récente :)
Klaas
1

Voici le wrapper de propriété atomique que j'utilise beaucoup. J'ai fait du véritable mécanisme de verrouillage un protocole, afin que je puisse expérimenter différents mécanismes. J'ai essayé les sémaphores DispatchQueues, et le pthread_rwlock_t. Le a pthread_rwlock_tété choisi car il semble avoir le surcoût le plus bas et une moindre chance d'inversion de priorité.

/// Defines a basic signature that all locks will conform to. Provides the basis for atomic access to stuff.
protocol Lock {
    init()
    /// Lock a resource for writing. So only one thing can write, and nothing else can read or write.
    func writeLock()
    /// Lock a resource for reading. Other things can also lock for reading at the same time, but nothing else can write at that time.
    func readLock()
    /// Unlock a resource
    func unlock()
}

final class PThreadRWLock: Lock {
    private var rwLock = pthread_rwlock_t()

    init() {
        guard pthread_rwlock_init(&rwLock, nil) == 0 else {
            preconditionFailure("Unable to initialize the lock")
        }
    }

    deinit {
        pthread_rwlock_destroy(&rwLock)
    }

    func writeLock() {
        pthread_rwlock_wrlock(&rwLock)
    }

    func readLock() {
        pthread_rwlock_rdlock(&rwLock)
    }

    func unlock() {
        pthread_rwlock_unlock(&rwLock)
    }
}

/// A property wrapper that ensures atomic access to a value. IE only one thing can write at a time.
/// Multiple things can potentially read at the same time, just not during a write.
/// By using `pthread` to do the locking, this safer then using a `DispatchQueue/barrier` as there isn't a chance
/// of priority inversion.
@propertyWrapper
public final class Atomic<Value> {

    private var value: Value
    private let lock: Lock = PThreadRWLock()

    public init(wrappedValue value: Value) {
        self.value = value
    }

    public var wrappedValue: Value {
        get {
            self.lock.readLock()
            defer { self.lock.unlock() }
            return self.value
        }
        set {
            self.lock.writeLock()
            self.value = newValue
            self.lock.unlock()
        }
    }

    /// Provides a closure that will be called synchronously. This closure will be passed in the current value
    /// and it is free to modify it. Any modifications will be saved back to the original value.
    /// No other reads/writes will be allowed between when the closure is called and it returns.
    public func mutate(_ closure: (inout Value) -> Void) {
        self.lock.writeLock()
        closure(&value)
        self.lock.unlock()
    }
}
jamone
la source
1

Depuis Swift 5.1, vous pouvez utiliser des wrappers de propriétés pour créer une logique spécifique pour vos propriétés. Ceci est l'implémentation du wrapper atomique:

@propertyWrapper
struct atomic<T> {
    private var value: T
    private let lock = NSLock()

    init(wrappedValue value: T) {
        self.value = value
    }

    var wrappedValue: T {
      get { getValue() }
      set { setValue(newValue: newValue) }
    }

    func getValue() -> T {
        lock.lock()
        defer { lock.unlock() }

        return value
    }

    mutating func setValue(newValue: T) {
        lock.lock()
        defer { lock.unlock() }

        value = newValue
    }
}

Comment utiliser:

class Shared {
    @atomic var value: Int
...
}
iUrii
la source