Sind Swift-Variablen atomar?


102

In Objective-C wird zwischen atomaren und nichtatomaren Eigenschaften unterschieden:

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

Nach meinem Verständnis können Sie Eigenschaften, die als atomar definiert sind, sicher aus mehreren Threads lesen und schreiben, während das gleichzeitige Schreiben und Zugreifen auf nichtatomare Eigenschaften oder ivars aus mehreren Threads zu undefiniertem Verhalten führen kann, einschließlich fehlerhafter Zugriffsfehler.

Wenn Sie also eine Variable wie diese in Swift haben:

var object: NSObject

Kann ich diese Variable sicher parallel lesen und schreiben? (Ohne die tatsächliche Bedeutung zu berücksichtigen).


Ich denke in Zukunft können wir vielleicht @atomicoder verwenden @nonatomic. oder standardmäßig nur atomar. (Swift ist so unvollständig, wir können jetzt nicht viel sagen)
Bryan Chen

1
IMO, sie machen standardmäßig alles nicht-atomar und bieten wahrscheinlich eine spezielle Funktion, um atomare Sachen zu machen.
Eonil

Abgesehen davon wird atomices im Allgemeinen nicht als ausreichend für eine thread-sichere Interaktion mit einer Eigenschaft angesehen, außer für einfache Datentypen. Bei Objekten wird der Zugriff über Threads im Allgemeinen mithilfe von Sperren (z. B. NSLockoder @synchronized) oder GCD-Warteschlangen (z. B. serielle Warteschlange oder gleichzeitige Warteschlange mit dem Muster "Leser-Schreiber") synchronisiert .
Rob

@Rob, true, obwohl aufgrund des Referenzzählens in Objective-C (und möglicherweise in Swift) das gleichzeitige Lesen und Schreiben in eine Variable ohne atomaren Zugriff zu einer Speicherbeschädigung führen kann. Wenn alle Variablen atomaren Zugriff hätten, wäre das Schlimmste, was passieren könnte, eine "logische" Rassenbedingung, dh unerwartetes Verhalten.
Lassej

Versteh mich nicht falsch: Ich hoffe, Apple beantwortet / löst die Frage nach dem atomaren Verhalten. Es ist nur so, dass (a) atomicdie Fadensicherheit für Objekte nicht gewährleistet; und (b) wenn man eine der oben genannten Synchronisationstechniken richtig verwendet, um die Thread-Sicherheit zu gewährleisten (unter anderem um gleichzeitiges Lesen / Schreiben zu verhindern), ist das atomare Problem umstritten. Aber wir brauchen / wollen es immer noch für einfache Datentypen, bei denen es atomiceinen echten Wert hat. Gute Frage!
Rob

Antworten:


52

Es ist sehr früh anzunehmen, dass keine Dokumentation auf niedriger Ebene verfügbar ist, Sie jedoch von der Montage aus lernen können. Hopper Disassembler ist ein großartiges Werkzeug.

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

Verwendet objc_storeStrongund objc_setProperty_atomicfür nichtatomare bzw. atomare, wobei

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

verwendet swift_retainvon libswift_stdlib_coreund hat anscheinend keine eingebaute Thread-Sicherheit.

Wir können spekulieren, dass später zusätzliche Schlüsselwörter (ähnlich wie @lazy) eingeführt werden könnten.

Update 20.07.15 : Laut diesem Blogpost auf Singletons kann eine schnelle Umgebung bestimmte Fälle für Sie sicher machen, dh:

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

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

}

Update 25.05.16 : Halten Sie Ausschau nach dem Vorschlag für eine schnelle Entwicklung https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - es sieht so aus Es wird möglich sein, das @atomicVerhalten selbst implementieren zu lassen.


Ich habe meine Antwort mit einigen aktuellen Informationen aktualisiert, hoffe es hilft
Sash Zats

1
Hey, danke für den Link zum Hopper Disassembler Tool. Sieht ordentlich aus.
C0D3

11

Swift hat keine Sprachkonstrukte zur Thread-Sicherheit. Es wird davon ausgegangen, dass Sie die bereitgestellten Bibliotheken verwenden, um Ihr eigenes Thread-Sicherheitsmanagement durchzuführen. Bei der Implementierung der Thread-Sicherheit stehen zahlreiche Optionen zur Verfügung, darunter pthread-Mutexe, NSLock und dispatch_sync als Mutex-Mechanismus. Siehe Mike Ashs jüngsten Beitrag zu diesem Thema: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Also die direkte Antwort auf Ihre Frage "Can Ich lese und schreibe sicher parallel zu dieser Variablen? " ist Nein.


7

Es ist wahrscheinlich zu früh, um diese Frage zu beantworten. Derzeit fehlen Swift Zugriffsmodifikatoren, sodass es keine offensichtliche Möglichkeit gibt, Code hinzuzufügen, der die Parallelität um einen Getter / Setter von Eigenschaften verwaltet. Darüber hinaus scheint die Swift-Sprache noch keine Informationen zur Parallelität zu haben! (Es fehlt auch KVO etc ...)

Ich denke, die Antwort auf diese Frage wird in zukünftigen Versionen klar.


re: Mangel an KVO, check out willSet, didSet- scheint ein erster Schritt auf dem Weg zu sein
Sash Zats

1
willSet, didSet ist eher für Eigenschaften gedacht, für die immer ein benutzerdefinierter Setter erforderlich war, weil sie etwas tun mussten. Beispiel: Eine Farbeigenschaft, die eine Ansicht neu zeichnen muss, wenn die Eigenschaft in einen anderen Wert geändert wird. Mit didSet ist das jetzt einfacher.
Gnasher729

Ja, das habe ich mit "einem ersten Schritt" gemeint :) Ich nahm an, dass dies ein Zeichen dafür sein könnte, dass die Funktion verfügbar, aber noch nicht vollständig implementiert ist
Sash Zats

6

Einzelheiten

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

Links

Implementierte Typen

Hauptidee

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

Beispiel für einen atomaren Zugang

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

Verwendung

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

Ergebnis

Geben Sie hier die Bildbeschreibung ein


Ein Beispielprojekt auf Github wäre schön!
Klaas

1
Hallo! Dies ist eine vollständige Probe. Kopieren Sie die AtomicKlasse und führen Sie sie mitAtomic().semaphoreSample()
Vasily Bodnarchuk

Ja, das habe ich schon getan. Ich dachte, es wäre schön, es als Projekt zu haben, das auf die aktuellste Syntax aktualisiert wird. Mit Swift ändert sich die Syntax ständig. Und Ihre Antwort ist bei weitem die aktuellste :)
Klaas

1

Ab Swift 5.1 können Sie Eigenschafts-Wrapper verwenden , um eine spezifische Logik für Ihre Eigenschaften zu erstellen. Dies ist eine atomare Wrapper-Implementierung:

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

Wie benutzt man:

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

0

Hier ist der Wrapper für atomare Eigenschaften, den ich häufig verwende. Ich habe den eigentlichen Sperrmechanismus zu einem Protokoll gemacht, damit ich mit verschiedenen Mechanismen experimentieren kann. Ich habe Semaphoren ausprobiert DispatchQueues, und die pthread_rwlock_t. Das pthread_rwlock_twurde ausgewählt, weil es den geringsten Overhead und eine geringere Wahrscheinlichkeit einer Prioritätsumkehr zu haben scheint.

/// 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()
    }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.