Чи є атомними змінні Swift?


102

У Objective-C ви маєте різницю між атомними та неатомними властивостями:

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

З мого розуміння, ви можете безпечно читати і записувати властивості, визначені як атомні з декількох потоків, в той час як запис і доступ до неатомічних властивостей або ivars з декількох потоків одночасно можуть призвести до не визначеної поведінки, включаючи помилкові помилки доступу.

Отже, якщо у Swift у вас є така змінна:

var object: NSObject

Чи можу я паралельно безпечно читати та записувати до цієї змінної? (Не враховуючи власне значення цього робити).


Я думаю, що в майбутньому, можливо, ми можемо використати @atomicабо @nonatomic. або просто атомний за замовчуванням. (Свіфт настільки незавершений, що ми не можемо сказати багато зараз)
Брайан Чен,

1
IMO, вони за замовчуванням зроблять все неатомним, і, ймовірно, нададуть особливу функцію виготовлення атомних матеріалів.
eonil

З іншого боку, atomicзазвичай не вважається достатнім для безпечної потокової взаємодії з властивістю, за винятком простих типів даних. Для об'єктів один, як правило, синхронізує доступ через потоки, використовуючи блокування (наприклад, NSLockабо @synchronized) або черги GCD (наприклад, послідовна черга або паралельна черга з шаблоном "читач-записувач").
Роб

@Rob, правда, хоча через підрахунок посилань в Objective-C (і, можливо, у Swift) одночасне читання та запис у змінну без атомного доступу може призвести до пошкодження пам'яті. Якби всі змінні мали атомний доступ, найгіршим, що могло статися, було б "логічний" стан перегонів, тобто несподівана поведінка.
lassej

Не зрозумійте мене неправильно: Я сподіваюся, що Apple відповість / вирішить питання про атомну поведінку. Просто (a) atomicне забезпечує безпеку ниток для об'єктів; і (b) якщо належним чином застосовується один із згаданих вище способів синхронізації для забезпечення безпеки потоку (крім усього іншого, запобігання одночасного читання / запису), атомна проблема є суперечливою. Але ми все ще потребуємо / хочемо це для простих типів даних, де atomicмає реальне значення. Гарне питання!
Роб

Відповіді:


52

Дуже рано припустити, що документація низького рівня недоступна, але ви можете вивчити її з монтажу. Розбірник хопера - чудовий інструмент.

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

Використання objc_storeStrongі objc_setProperty_atomicдля неатомних, і для атомних відповідно, де

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

використання swift_retainвід libswift_stdlib_coreі, по- видимому, не має потокобезпечна вбудовані.

Ми можемо припустити, що додаткові ключові слова (подібні до @lazy) можуть бути введені пізніше.

Оновлення 20.07.2015 : згідно з цим поштовим блогом у одиночних умовах швидке середовище може зробити певні випадки безпечними для вас, тобто:

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

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

}

Оновлення 25.05.16 : Слідкуйте за швидкою пропозицією про еволюцію https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - схоже, що це стане можливим, щоб @atomicповедінку реалізовував сам.


Я оновив свою відповідь деякою нещодавньою інформацією, сподіваюся, що це допоможе
Sash Zats

1
Гей, дякую за посилання на інструмент розбирання хоперів. Виглядає акуратно.
C0D3

11

У Swift немає мовних конструкцій навколо безпеки потоку. Передбачається, що ви будете використовувати надані бібліотеки для власного управління безпекою потоку. Існує велика кількість варіантів у застосуванні безпеки потоку, включаючи pthread mutexes, NSLock та dispatch_sync як механізм mutex. Дивіться нещодавнє повідомлення Майка Еша на цю тему: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Тож прямий відповідь на ваше запитання "Можна Я паралельно безпечно читаю і пишу до цієї змінної? " є Ні.


7

Напевно, рано відповісти на це питання. Наразі у швидкого бракує модифікаторів доступу, тому немає очевидного способу додати код, який керує паралельністю навколо гетьєра / сеттера властивостей. Більше того, у мови Swift, схоже, ще немає інформації про одночасність! (Також не вистачає KVO тощо)

Я думаю, що відповідь на це питання стане зрозумілою у майбутніх випусках.


re: відсутність KVO, перевіряйте willSet, didSet- здається, це перший крок на шляху
Sash Zats

1
willSet, didSet - це більше для властивостей, які завжди потребували налаштованого налаштування, оскільки вони повинні були щось робити. Наприклад, властивість кольору, якій потрібно перемальовувати подання, коли властивість змінюється на інше значення; що зараз робиться простіше за допомогою didSet.
gnasher729

так, саме це я мав на увазі під "першим кроком" :) Я припустив, що це може бути ознакою доступності функції, але ще не повністю реалізованої
Sash Zats

6

Деталі

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

Посилання

Реалізовані типи

Головна думка

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

Зразок атомного доступу

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

Використання

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

Результат

введіть тут опис зображення


Зразок проекту на Github був би приємний!
Клаас

1
Привіт! Це повний зразок. Скопіюйте Atomicклас та запустіть його за допомогоюAtomic().semaphoreSample()
Василя Боднарчука

Так, я вже зробив. Думав, що було б добре, щоб це було як проект, який оновлюється до найактуальнішого синтаксису. З Swift синтаксис постійно змінюється. І ваша відповідь на сьогоднішній день є найсвіжішою :)
Клаас

1

З Swift 5.1 ви можете використовувати власні обгортки для створення конкретної логіки для ваших властивостей. Це реалізація атомної обгортки:

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

Як використовувати:

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

0

Ось обгортка властивостей атомних властивостей, яку я широко використовую. Я зробив фактичний механізм блокування протоколом, тому я міг відчувати різні механізми. Я спробував семафори DispatchQueues, та pthread_rwlock_t. pthread_rwlock_tБув обраний тому , що , як видається , мають найнижчу навантаження, і менше шансів на інверсії пріоритетів.

/// 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()
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.