ตัวแปร Swift เป็นอะตอมหรือไม่?


102

ใน Objective-C คุณมีความแตกต่างระหว่างคุณสมบัติของอะตอมและคุณสมบัติที่ไม่ใช่อะตอม:

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

จากความเข้าใจของฉันคุณสามารถอ่านและเขียนคุณสมบัติที่กำหนดเป็นอะตอมจากหลายเธรดได้อย่างปลอดภัยในขณะที่การเขียนและการเข้าถึงคุณสมบัติที่ไม่ใช่อะตอมหรือ ivars จากหลายเธรดในเวลาเดียวกันอาจทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดรวมถึงข้อผิดพลาดในการเข้าถึงที่ไม่ดี

ดังนั้นหากคุณมีตัวแปรเช่นนี้ใน Swift:

var object: NSObject

ฉันสามารถอ่านและเขียนถึงตัวแปรนี้แบบขนานอย่างปลอดภัยได้หรือไม่? (โดยไม่ได้คำนึงถึงความหมายที่แท้จริงของการทำเช่นนี้).


ผมคิดว่าในอนาคตเราอาจจะสามารถใช้หรือ@atomic @nonatomicหรือแค่ปรมาณูโดยปริยาย (Swift ยังไม่สมบูรณ์เราไม่สามารถบอกอะไรได้มากในตอนนี้)
Bryan Chen

1
IMO พวกเขาจะทำให้ทุกอย่างไม่ใช่อะตอมตามค่าเริ่มต้นและอาจมีคุณสมบัติพิเศษในการสร้างอะตอม
eonil

นอกจากatomicนี้โดยทั่วไปถือว่าไม่เพียงพอสำหรับการโต้ตอบกับเธรดเซฟตี้กับพร็อพเพอร์ตี้ยกเว้นประเภทข้อมูลธรรมดา สำหรับอ็อบเจ็กต์โดยทั่วไปแล้วอ็อบเจ็กต์หนึ่งจะซิงโครไนซ์การเข้าถึงข้ามเธรดโดยใช้การล็อก (เช่นNSLockหรือ@synchronized) หรือคิว GCD (เช่นคิวแบบอนุกรมหรือคิวพร้อมกันโดยใช้รูปแบบ "ตัวอ่าน - ตัวเขียน")
Rob

@Rob จริงแม้ว่าเนื่องจากการอ้างอิงการนับใน Objective-C (และอาจเป็นใน Swift) การอ่านและเขียนพร้อมกันไปยังตัวแปรโดยไม่ต้องเข้าถึงอะตอมอาจทำให้หน่วยความจำเสียหายได้ หากตัวแปรทั้งหมดมีการเข้าถึงปรมาณูสิ่งที่เลวร้ายที่สุดที่อาจเกิดขึ้นได้ก็คือสภาพการแข่งขันแบบ "ตรรกะ" นั่นคือพฤติกรรมที่ไม่คาดคิด
lassej

อย่าเข้าใจฉันผิด: ฉันหวังว่า Apple จะตอบ / แก้ไขคำถามเกี่ยวกับพฤติกรรมปรมาณู เป็นเพียงว่า (ก) atomicไม่มั่นใจในความปลอดภัยของเธรดสำหรับวัตถุ และ (b) หากเราใช้เทคนิคการซิงโครไนซ์อย่างใดอย่างหนึ่งข้างต้นอย่างถูกต้องเพื่อให้แน่ใจว่าเธรดปลอดภัย (นอกเหนือจากสิ่งอื่น ๆ การป้องกันการอ่าน / เขียนพร้อมกัน) ปัญหาเกี่ยวกับอะตอมก็คือปัญหา แต่เรายังต้องการ / ต้องการสำหรับประเภทข้อมูลแบบธรรมดาที่atomicมีมูลค่าจริง คำถามที่ดี!
Rob

คำตอบ:


52

ยังเร็วมากที่จะถือว่าไม่มีเอกสารระดับต่ำ แต่คุณสามารถศึกษาได้จากการประกอบ Hopper Disassemblerเป็นเครื่องมือที่ยอดเยี่ยม

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

ใช้objc_storeStrongและobjc_setProperty_atomicสำหรับ nonatomic และ atomic ตามลำดับโดยที่

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

การใช้งานswift_retainจากlibswift_stdlib_coreและเห็นได้ชัดว่าไม่มีความปลอดภัยของเธรดในตัว

เราสามารถคาดเดาได้ว่า@lazyอาจมีการนำคำหลักเพิ่มเติม (ที่คล้ายกับ) มาใช้ในภายหลัง

อัปเดต 07/20/15 : ตามบล็อกโพสต์นี้เกี่ยวกับสภาพแวดล้อมที่รวดเร็วของsingletonsสามารถทำให้เธรดบางกรณีปลอดภัยสำหรับคุณเช่น:

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

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

}

อัปเดต 05/25/16 : จับตาดูข้อเสนอวิวัฒนาการที่รวดเร็วhttps://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md - ดูเหมือนว่าจะเป็น เป็นไปได้ที่จะนำ@atomicพฤติกรรมไปปฏิบัติด้วยตัวเอง


ฉันอัปเดตคำตอบด้วยข้อมูลล่าสุดหวังว่าจะช่วยได้
Sash Zats

1
สวัสดีขอบคุณสำหรับลิงก์ไปยังเครื่องมือ Hopper Disassembler ดูเรียบร้อย.
C0D3

11

Swift ไม่มีโครงสร้างภาษาเกี่ยวกับความปลอดภัยของเธรด สันนิษฐานว่าคุณจะใช้ไลบรารีที่จัดเตรียมไว้เพื่อจัดการความปลอดภัยเธรดของคุณเอง มีตัวเลือกมากมายที่คุณมีในการใช้งานความปลอดภัยของเธรดซึ่งรวมถึง pthread mutexes, NSLock และ dispatch_sync เป็นกลไก mutex ดูโพสต์ล่าสุดของ Mike Ash ในหัวข้อ: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html ดังนั้นคำตอบโดยตรงสำหรับคำถามของคุณเรื่อง "Can ฉันอ่านและเขียนถึงตัวแปรนี้ควบคู่กันอย่างปลอดภัยหรือไม่ " ไม่ใช่


7

อาจเป็นไปได้ก่อนที่จะตอบคำถามนี้ ปัจจุบันรวดเร็วไม่มีตัวปรับแต่งการเข้าถึงดังนั้นจึงไม่มีวิธีที่ชัดเจนในการเพิ่มโค้ดที่จัดการการทำงานพร้อมกันรอบ ๆ คุณสมบัติ getter / setter นอกจากนี้ภาษา 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 จะดีมาก!
Klaas

1
สวัสดี! นี่คือตัวอย่างฉบับเต็ม คัดลอกAtomicชั้นเรียนและเรียกใช้โดยใช้Atomic().semaphoreSample()
Vasily Bodnarchuk

ใช่ฉันทำไปแล้ว คิดว่าคงจะดีถ้าจะให้เป็นโปรเจ็กต์ที่ได้รับการอัปเดตเป็นไวยากรณ์ที่เป็นปัจจุบันที่สุด ด้วย Swift ไวยากรณ์จะเปลี่ยนแปลงตลอดเวลา และคำตอบของคุณคือคำตอบล่าสุด :)
Klaas

1

นี่คือกระดาษห่อสมบัติอะตอมที่ฉันใช้อย่างกว้างขวาง ฉันสร้างกลไกการล็อคที่แท้จริงเป็นโปรโตคอลดังนั้นฉันจึงสามารถทดลองใช้กลไกต่างๆได้ ฉันลองใช้ semaphores 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()
    }
}

1

จาก Swift 5.1 คุณสามารถใช้ตัวตัดคุณสมบัติเพื่อสร้างตรรกะเฉพาะสำหรับคุณสมบัติของคุณ นี่คือการใช้งาน 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
    }
}

วิธีใช้:

class Shared {
    @atomic var value: Int
...
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.