¿Son atómicas las variables de Swift?

102

En Objective-C tiene una distinción entre propiedades atómicas y no atómicas:

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

Según tengo entendido, puede leer y escribir propiedades definidas como atómicas de varios subprocesos de forma segura, mientras que escribir y acceder a propiedades no atómicas o ivars de varios subprocesos al mismo tiempo puede provocar un comportamiento indefinido, incluidos errores de acceso incorrecto.

Entonces, si tiene una variable como esta en Swift:

var object: NSObject

¿Puedo leer y escribir en esta variable en paralelo de forma segura? (Sin considerar el significado real de hacer esto).

lassej
fuente
Creo que en el futuro, tal vez podamos usar @atomico @nonatomic. o simplemente atómico por defecto. (Swift está tan incompleto que no podemos decir mucho ahora)
Bryan Chen
1
En mi opinión, harán que todo no sea atómico de forma predeterminada y probablemente proporcionen una característica especial para hacer cosas atómicas.
eonil
Como acotación al margen, atomicgeneralmente no se considera suficiente para la interacción segura para subprocesos con una propiedad, excepto para tipos de datos simples. Para los objetos, generalmente se sincroniza el acceso a través de subprocesos utilizando bloqueos (p. Ej., NSLockO @synchronized) o colas GCD (p. Ej., Cola en serie o cola simultánea con patrón "lector-escritor").
Rob
@Rob, cierto, aunque debido al recuento de referencias en Objective-C (y posiblemente en Swift), la lectura y escritura simultáneas en una variable sin acceso atómico puede provocar daños en la memoria. Si todas las variables tuvieran acceso atómico, lo peor que podría suceder sería una condición de carrera "lógica", es decir, un comportamiento inesperado.
lassej
No me malinterpretes: espero que Apple responda / resuelva la pregunta sobre el comportamiento atómico. Es solo que (a) atomicno garantiza la seguridad de los subprocesos para los objetos; y (b) si uno usa correctamente una de las técnicas de sincronización mencionadas anteriormente para garantizar la seguridad de los subprocesos (entre otras cosas, evitando lecturas / escrituras simultáneas), el problema atómico es discutible. Pero todavía lo necesitamos / queremos para tipos de datos simples, donde atomictiene un valor real. ¡Buena pregunta!
Rob

Respuestas:

52

Es muy temprano para asumir que no hay documentación de bajo nivel disponible, pero puede estudiar desde el ensamblaje. El desensamblador de tolva es una gran herramienta.

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

Usos objc_storeStrongy objc_setProperty_atomicpara atómicos y no atómicos respectivamente, donde

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

usa swift_retaindesde libswift_stdlib_corey, aparentemente, no tiene seguridad para subprocesos incorporada.

Podemos especular que más @lazyadelante podrían introducirse palabras clave adicionales (similares a ).

Actualización 20/07/15 : de acuerdo con esta publicación de blog sobre singletons, el entorno rápido puede hacer que ciertos casos sean seguros para usted, es decir:

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

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

}

Actualización 25/05/16 : Esté atento a la propuesta de evolución rápida https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - parece que lo es será posible que @atomicusted mismo implemente el comportamiento.

Faja Zats
fuente
Actualicé
1
Hola, gracias por el enlace a la herramienta Desmontador de tolva. Se ve bien.
C0D3
11

Swift no tiene construcciones de lenguaje en torno a la seguridad de subprocesos. Se supone que utilizará las bibliotecas proporcionadas para realizar su propia gestión de seguridad de subprocesos. Hay una gran cantidad de opciones que tiene para implementar la seguridad de subprocesos, incluidos los mutex de pthread, NSLock y dispatch_sync como mecanismo de exclusión mutua. Consulte la publicación reciente de Mike Ash sobre el tema: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Entonces, la respuesta directa a su pregunta de "¿Puede ¿Leo y escribo en esta variable en paralelo de forma segura? " no es.

Buen Doug
fuente
7

Probablemente sea demasiado pronto para responder a esta pregunta. Actualmente, Swift carece de modificadores de acceso, por lo que no hay una forma obvia de agregar código que gestione la concurrencia en torno a un captador / definidor de propiedades. Además, Swift Language todavía no parece tener información sobre la concurrencia. (También carece de KVO, etc.)

Creo que la respuesta a esta pregunta quedará clara en futuras versiones.

ColinE
fuente
re: falta de KVO, echa un vistazo willSet, didSet- parece ser un primer paso en el camino
Sash Zats
1
willSet, didSet es más para propiedades que siempre necesitaron un definidor personalizado porque tenían que hacer algo. Por ejemplo, una propiedad de color que necesita volver a dibujar una vista cuando la propiedad se cambia a un valor diferente; eso ahora se hace más fácil con didSet.
gnasher729
sí, eso es lo que quise decir con "un primer paso" :) Supuse que podría ser una señal de que la función está disponible pero aún no está completamente implementada
Sash Zats
6

Detalles

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

Enlaces

Tipos implementados

Idea principal

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

Muestra de acceso atómico

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

Uso

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

Resultado

ingrese la descripción de la imagen aquí

Vasily Bodnarchuk
fuente
¡Un proyecto de muestra en github estaría bien!
Klaas
1
¡Hola! Esta es una muestra completa. Copie la Atomicclase y ejecútela usandoAtomic().semaphoreSample()
Vasily Bodnarchuk
Sí, ya lo hice. Pensé que sería bueno tenerlo como un proyecto que se actualiza a la sintaxis más actual. Con Swift la sintaxis cambia todo el tiempo. Y tu respuesta es, con mucho, la más reciente :)
Klaas
1

Desde Swift 5.1 puede utilizar envoltorios de propiedades para crear una lógica específica para sus propiedades. Esta es la implementación del contenedor atómico:

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

Cómo utilizar:

class Shared {
    @atomic var value: Int
...
}
iUrii
fuente
0

Aquí está el contenedor de propiedades atómicas que utilizo ampliamente. Hice un protocolo del mecanismo de bloqueo real, para poder experimentar con diferentes mecanismos. Probé con semáforos DispatchQueues, y pthread_rwlock_t. Se pthread_rwlock_teligió porque parece tener la sobrecarga más baja y una menor probabilidad de una inversión de prioridad.

/// 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
fuente