Czy zmienne Swift są atomowe?

102

W Objective-C masz rozróżnienie między właściwościami atomowymi i nieatomowymi:

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

Z mojego zrozumienia można bezpiecznie odczytywać i zapisywać właściwości zdefiniowane jako atomowe z wielu wątków, podczas gdy pisanie i uzyskiwanie dostępu do właściwości nieatomowych lub ivars z wielu wątków w tym samym czasie może skutkować niezdefiniowanym zachowaniem, w tym błędami złego dostępu.

Więc jeśli masz taką zmienną w Swift:

var object: NSObject

Czy mogę bezpiecznie odczytywać i zapisywać w tej zmiennej równolegle? (Bez rozważania rzeczywistego znaczenia robienia tego).

lassej
źródło
Myślę, że w przyszłości możemy użyć @atomiclub @nonatomic. lub domyślnie atomic. (Swift jest tak niekompletny, nie możemy teraz wiele powiedzieć)
Bryan Chen,
1
IMO, domyślnie uczynią wszystko nieatomowym i prawdopodobnie zapewnią specjalną funkcję do tworzenia materiałów atomowych.
eonil
Na marginesie, atomicgeneralnie nie jest uważany za wystarczający do bezpiecznej dla wątków interakcji z właściwością, z wyjątkiem prostych typów danych. W przypadku obiektów zazwyczaj synchronizuje się dostęp między wątkami za pomocą blokad (np. NSLockLub @synchronized) lub kolejek GCD (np. Kolejka szeregowa lub kolejka współbieżna ze wzorcem „czytnik-zapisujący”).
Rob
@Rob, prawda, chociaż z powodu liczenia odwołań w Objective-C (i prawdopodobnie w Swift), jednoczesne odczytywanie i zapisywanie do zmiennej bez dostępu atomowego może spowodować uszkodzenie pamięci. Gdyby wszystkie zmienne miały atomowy dostęp, najgorszą rzeczą, jaka mogłaby się zdarzyć, byłby „logiczny” wyścig, tj. Nieoczekiwane zachowanie.
lassej,
Nie zrozum mnie źle: mam nadzieję, że Apple odpowie na pytanie dotyczące zachowania atomowego. Po prostu (a) atomicnie zapewnia bezpieczeństwa wątków dla obiektów; oraz (b) jeśli ktoś właściwie używa jednej z wyżej wymienionych technik synchronizacji w celu zapewnienia bezpieczeństwa wątków (między innymi, zapobieganie jednoczesnemu odczytowi / zapisowi), kwestia atomowa jest dyskusyjna. Ale nadal potrzebujemy / chcemy tego dla prostych typów danych, gdzie atomicma realną wartość. Dobre pytanie!
Rob,

Odpowiedzi:

52

Jest bardzo wcześnie, aby założyć, że nie jest dostępna dokumentacja niskiego poziomu, ale można uczyć się od montażu. Disassembler hopper to świetne narzędzie.

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

Używa objc_storeStrongi odpowiednio objc_setProperty_atomicdla nieatomowych i atomowych, gdzie

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

używa swift_retainod libswift_stdlib_corei najwyraźniej nie ma wbudowanego zabezpieczenia wątków.

Możemy spekulować, że dodatkowe słowa kluczowe (podobne do @lazy) mogą zostać wprowadzone później.

Aktualizacja 20.07.15 : zgodnie z tym postem na blogu na singletonach, środowisko swift może sprawić, że niektóre przypadki będą bezpieczne dla Ciebie, np .:

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

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

}

Aktualizacja 25.05.16 : Wypatruj propozycji szybkiej ewolucji https://github.com/apple/swift-evolution/blob/master/propiments/0030-property-behavior-decls.md - wygląda na to, że tak jest będziesz mógł mieć @atomiczaimplementowane zachowanie.

Sash Zats
źródło
Zaktualizowałem moją odpowiedź kilkoma ostatnimi informacjami, mam nadzieję, że to pomoże
Sash Zats
1
Hej, dzięki za link do narzędzia do demontażu zbiorników. Wygląda schludnie.
C0D3
11

Swift nie ma żadnych konstrukcji językowych związanych z bezpieczeństwem wątków. Zakłada się, że będziesz używać dostarczonych bibliotek do zarządzania bezpieczeństwem wątków. Istnieje wiele opcji implementacji zabezpieczeń wątków, w tym muteksy pthread, NSLock i dispatch_sync jako mechanizm mutex. Zobacz ostatni post Mike'a Asha na ten temat: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html A więc bezpośrednia odpowiedź na pytanie „Czy Czy czytam i piszę do tej zmiennej równolegle bezpiecznie? ” nie jest.

Dobry Doug
źródło
7

Prawdopodobnie jest za wcześnie, aby odpowiedzieć na to pytanie. Obecnie w Swift brakuje modyfikatorów dostępu, więc nie ma oczywistego sposobu na dodanie kodu, który zarządza współbieżnością wokół metody pobierającej / ustawiającej właściwości. Co więcej, język Swift nie wydaje się jeszcze mieć żadnych informacji o współbieżności! (Brakuje również KVO itp.)

Myślę, że odpowiedź na to pytanie stanie się jasna w przyszłych wydaniach.

ColinE
źródło
re: brak KVO, sprawdź willSet, didSet- wydaje się być pierwszym krokiem na drodze
Sash Zats
1
willSet, didSet jest bardziej dla właściwości, które zawsze potrzebowały niestandardowego ustawiacza, ponieważ musiały coś zrobić. Na przykład właściwość koloru, która wymaga przerysowania widoku, gdy właściwość zostanie zmieniona na inną wartość; jest to teraz łatwiejsze dzięki didSet.
gnasher729
tak, to właśnie miałem na myśli mówiąc „pierwszy krok” :) Przypuszczałem, że może to oznaczać, że funkcja jest dostępna, ale nie jest jeszcze w pełni zaimplementowana
Sash Zats
6

Detale

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

Spinki do mankietów

Wdrożone typy

Główny pomysł

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

Próbka dostępu atomowego

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

Stosowanie

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

Wynik

wprowadź opis obrazu tutaj

Wasilij Bodnarczuk
źródło
Przykładowy projekt na github byłby fajny!
Klaas
1
Witaj! To jest pełna próbka. Skopiuj Atomicklasę i uruchom ją za pomocąAtomic().semaphoreSample()
Wasilij Bodnarchuk
Tak, już to zrobiłem. Pomyślałem, że byłoby fajnie mieć go jako projekt, który jest aktualizowany do najnowszej składni. W przypadku Swift składnia zmienia się cały czas. A twoja odpowiedź jest jak dotąd ostatnia :)
Klaas
1

Oto opakowanie właściwości atomowych, którego często używam. Zrobiłem rzeczywisty mechanizm blokujący jako protokół, więc mogłem eksperymentować z różnymi mechanizmami. Wypróbowałem semafory DispatchQueuesi pthread_rwlock_t. pthread_rwlock_tZostał wybrany, ponieważ wydaje się mieć najmniejszy narzut i mniejsze szanse na priorytetową inwersji.

/// 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
źródło
1

Od wersji Swift 5.1 możesz używać opakowań właściwości, aby tworzyć określoną logikę dla swoich właściwości. Oto implementacja atomic 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
    }
}

Jak używać:

class Shared {
    @atomic var value: Int
...
}
iUrii
źródło