Le variabili Swift sono atomiche?


102

In Objective-C hai una distinzione tra proprietà atomiche e non atomiche:

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

Dalla mia comprensione è possibile leggere e scrivere proprietà definite come atomiche da più thread in modo sicuro, mentre la scrittura e l'accesso a proprietà non atomiche o ivar da più thread contemporaneamente può comportare un comportamento indefinito, inclusi errori di accesso non valido.

Quindi, se hai una variabile come questa in Swift:

var object: NSObject

Posso leggere e scrivere in parallelo su questa variabile in modo sicuro? (Senza considerare l'effettivo significato di farlo).


Penso che in futuro, forse possiamo usare @atomico @nonatomic. o solo atomico per impostazione predefinita. (Swift è così incompleto, non possiamo dire molto ora)
Bryan Chen

1
IMO, renderanno tutto non atomico per impostazione predefinita e probabilmente forniranno una funzionalità speciale per creare cose atomiche.
eonil

Per inciso, atomicgeneralmente non è considerato sufficiente per l'interazione thread-safe con una proprietà, ad eccezione dei tipi di dati semplici. Per gli oggetti, in genere si sincronizza l'accesso attraverso i thread utilizzando blocchi (ad esempio, NSLocko @synchronized) o code GCD (ad esempio, coda seriale o coda simultanea con pattern "lettore-scrittore").
Rob il

@ Rob, true, sebbene a causa del conteggio dei riferimenti in Objective-C (e possibilmente in Swift), la lettura e la scrittura simultanee su una variabile senza accesso atomico possono provocare il danneggiamento della memoria. Se tutte le variabili avessero accesso atomico, la cosa peggiore che potrebbe accadere sarebbe una condizione di gara "logica", cioè un comportamento inaspettato.
lassej

Non fraintendetemi: spero che Apple risponda / risolva la domanda sul comportamento atomico. È solo che (a) atomicnon garantisce la sicurezza dei thread per gli oggetti; e (b) se si utilizza correttamente una delle suddette tecniche di sincronizzazione per garantire la sicurezza dei thread (tra le altre cose, impedire letture / scritture simultanee), la questione atomica è discutibile. Ma ne abbiamo ancora bisogno / lo vogliamo per tipi di dati semplici, dove atomicha un valore reale. Buona domanda!
Rob il

Risposte:


52

È molto presto per presumere poiché non è disponibile alcuna documentazione di basso livello, ma puoi studiare dall'assembly. Hopper Disassembler è un ottimo strumento.

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

Utilizza objc_storeStronge rispettivamente objc_setProperty_atomicper non atomico e atomico, dove

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

utilizza swift_retainda libswift_stdlib_coree, a quanto pare, non ha la sicurezza dei thread incorporata.

Possiamo ipotizzare che parole chiave aggiuntive (simili a @lazy) potrebbero essere introdotte in seguito.

Aggiornamento 20/07/15 : secondo questo post del blog su Singleton, l' ambiente swift può rendere sicuri i thread di alcuni casi, ad esempio:

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

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

}

Aggiornamento 25/05/16 : tieni d'occhio la rapida proposta di evoluzione https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - sembra che lo sia sarà possibile @atomicimplementare un comportamento da solo.


Ho aggiornato la mia risposta con alcune informazioni recenti, spero che aiuti
Sash Zats

1
Ehi, grazie per il collegamento allo strumento Hopper Disassembler. Sembra pulito.
C0D3

11

Swift non ha costrutti linguistici sulla sicurezza dei thread. Si presume che utilizzerai le librerie fornite per eseguire la tua gestione della sicurezza dei thread. Ci sono un gran numero di opzioni che hai per implementare la sicurezza dei thread, inclusi pthread mutex, NSLock e dispatch_sync come meccanismo mutex. Vedi il recente post di Mike Ash sull'argomento: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Quindi la risposta diretta alla tua domanda di "Can Ho letto e scritto su questa variabile in parallelo in modo sicuro? " è no.


7

Probabilmente è troppo presto per rispondere a questa domanda. Attualmente swift non ha modificatori di accesso, quindi non esiste un modo ovvio per aggiungere codice che gestisce la concorrenza attorno a un getter / setter di proprietà. Inoltre, Swift Language non sembra avere ancora informazioni sulla concorrenza! (Manca anche KVO ecc ...)

Penso che la risposta a questa domanda diventerà chiara nelle versioni future.


re: mancanza di KVO, controlla willSet, didSet- sembra essere un primo passo sulla strada
Sash Zats

1
willSet, didSet è più per le proprietà che hanno sempre avuto bisogno di un setter personalizzato perché dovevano fare qualcosa. Ad esempio, una proprietà color che deve ridisegnare una vista quando la proprietà viene modificata in un valore diverso; ora è più facile usando didSet.
gnasher729

sì, questo è ciò che intendevo con "un primo passo" :) Ho pensato che potesse essere un segno che la funzione fosse disponibile ma non ancora completamente implementata
Sash Zats

6

Dettagli

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

link

Tipi implementati

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

Campione di accesso atomico

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

Risultato

inserisci qui la descrizione dell'immagine


Un progetto di esempio su GitHub sarebbe carino!
Klaas

1
Ciao! Questo è un campione completo. Copia la Atomicclasse ed Atomic().semaphoreSample()
eseguila

Sì, l'ho già fatto. Ho pensato che sarebbe stato bello averlo come progetto aggiornato alla sintassi più recente. Con Swift la sintassi cambia continuamente. E la tua risposta è di gran lunga la più recente :)
Klaas

1

Da Swift 5.1 puoi utilizzare i wrapper di proprietà per creare una logica specifica per le tue proprietà. Questa è l'implementazione del wrapper atomico:

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

Come usare:

class Shared {
    @atomic var value: Int
...
}

0

Ecco il wrapper della proprietà atomica che uso ampiamente. Ho trasformato il meccanismo di blocco vero e proprio in un protocollo, così ho potuto sperimentare diversi meccanismi. Ho provato i semafori DispatchQueues, e il file pthread_rwlock_t. È pthread_rwlock_tstato scelto perché sembra avere l'overhead più basso e una minore possibilità di inversione di 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()
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.