Zijn Swift-variabelen atomair?

In Objective-C heb je een onderscheid tussen atomaire en niet-atomaire eigenschappen:

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

Voor zover ik weet kun je eigenschappen die als atomair zijn gedefinieerd vanuit meerdere threads veilig lezen en schrijven, terwijl het schrijven en openen van niet-atomaire eigenschappen of ivars van meerdere threads tegelijkertijd kan leiden tot ongedefinieerd gedrag, inclusief fouten bij slechte toegang.

Dus als je een variabele als deze hebt in Swift:

var object: NSObject

Kan ik deze variabele veilig parallel lezen en schrijven? (Zonder de werkelijke betekenis hiervan te overwegen).


Antwoord 1, autoriteit 100%

Het is erg vroeg om aan te nemen dat er geen low-level documentatie beschikbaar is, maar je kunt het bestuderen vanuit de assembly. Hopper Disassembleris een geweldig hulpmiddel.

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

Gebruikt objc_storeStrongen objc_setProperty_atomicvoor respectievelijk niet-atomair en atomair, waarbij

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

gebruikt swift_retainvan libswift_stdlib_coreen heeft blijkbaar geen ingebouwde threadbeveiliging.

We kunnen speculeren dat er later aanvullende zoekwoorden (vergelijkbaar met @lazy) zullen worden geïntroduceerd.

Update 20/07/15: volgens deze blogpost op singletonseen snelle omgeving kan bepaalde zaken veilig voor je maken, zoals:

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: houd het snelle evolutievoorstel in de gaten https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md– het lijkt erop het zal mogelijk zijn om @atomicgedrag door uzelf te laten implementeren.


Antwoord 2, autoriteit 20%

Swift heeft geen taalconstructies rond thread-veiligheid. Er wordt van uitgegaan dat u de verstrekte bibliotheken zult gebruiken om uw eigen threadveiligheidsbeheer te doen.
Er zijn een groot aantal opties die je hebt bij het implementeren van thread-veiligheid, waaronder pthread mutexes, NSLock en dispatch_sync als mutex-mechanisme. Zie het recente bericht van Mike Ash over dit onderwerp: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html
Dus het directe antwoord op uw vraag “Kan ik deze variabele veilig parallel lezen en schrijven?” is Nee.


Antwoord 3, autoriteit 13%

Het is waarschijnlijk te vroeg om deze vraag te beantwoorden. Swift heeft momenteel geen toegangsmodifiers, dus er is geen voor de hand liggende manier om code toe te voegen die gelijktijdigheid rond een eigenschappen-getter / setter beheert. Verder lijkt de Swift Language nog geen informatie te hebben over concurrency! (Het mist ook KVO enz …)

Ik denk dat het antwoord op deze vraag in toekomstige releases duidelijk zal worden.


Antwoord 4, autoriteit 11%

Details

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

Links

Geïmplementeerde typen

Hoofdidee

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

Voorbeeld van atomaire toegang

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

Gebruik

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

Resultaat

voer hier de afbeeldingsbeschrijving in


Antwoord 5, autoriteit 7%

Vanaf Swift 5.1 kunt u property-wrappersgebruiken om specifieke logica voor uw eigenschappen te maken. Dit is de implementatie van een atomaire wrapper:

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

Hoe te gebruiken:

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

Antwoord 6, autoriteit 6%

Hier is de wrapper met atomaire eigenschappen die ik veel gebruik. Ik heb van het eigenlijke vergrendelingsmechanisme een protocol gemaakt, zodat ik met verschillende mechanismen kon experimenteren. Ik heb semaforen geprobeerd, DispatchQueuesen de pthread_rwlock_t. De pthread_rwlock_tis gekozen omdat deze de laagste overhead lijkt te hebben en een kleinere kans op een omkering van de prioriteit.

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

Other episodes