Các biến Swift có phải là nguyên tử không?


102

Trong Objective-C, bạn có sự phân biệt giữa các tính chất nguyên tử và không giải phẫu:

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

Theo hiểu biết của tôi, bạn có thể đọc và ghi các thuộc tính được định nghĩa là nguyên tử từ nhiều luồng một cách an toàn, trong khi việc viết và truy cập các thuộc tính phi giải phẫu hoặc ivars từ nhiều luồng cùng một lúc có thể dẫn đến hành vi không xác định, bao gồm cả lỗi truy cập xấu.

Vì vậy, nếu bạn có một biến như thế này trong Swift:

var object: NSObject

Tôi có thể đọc và ghi song song biến này một cách an toàn không? (Mà không cần xem xét ý nghĩa thực tế của việc làm này).


Tôi nghĩ trong tương lai, có lẽ chúng ta có thể sử dụng @atomichoặc @nonatomic. hoặc chỉ nguyên tử theo mặc định. (Swift là rất không đầy đủ, chúng tôi không thể nói nhiều bây giờ)
Bryan Chen

1
IMO, họ sẽ làm cho mọi thứ không phải là nguyên tử theo mặc định và có thể cung cấp một tính năng đặc biệt để tạo ra những thứ nguyên tử.
eonil

Ngoài ra, atomicthường không được coi là đủ để tương tác an toàn theo luồng với một thuộc tính, ngoại trừ các kiểu dữ liệu đơn giản. Đối với các đối tượng, người ta thường đồng bộ hóa quyền truy cập qua các luồng bằng cách sử dụng khóa (ví dụ: NSLockhoặc @synchronized) hoặc hàng đợi GCD (ví dụ: hàng đợi nối tiếp hoặc hàng đợi đồng thời với mẫu "trình đọc-ghi").
Rob

@Rob, true, mặc dù do việc đếm tham chiếu trong Objective-C (và có thể trong Swift) đồng thời đọc và ghi vào một biến mà không có quyền truy cập nguyên tử có thể dẫn đến hỏng bộ nhớ. Nếu tất cả các biến đều có quyền truy cập nguyên tử, điều tồi tệ nhất có thể xảy ra sẽ là điều kiện chạy đua "hợp lý", tức là hành vi không mong muốn.
lassej

Đừng hiểu sai ý tôi: Tôi hy vọng Apple trả lời / giải quyết câu hỏi về hành vi nguyên tử. Chỉ là (a) atomickhông đảm bảo an toàn luồng cho các đối tượng; và (b) nếu người ta sử dụng đúng một trong các kỹ thuật đồng bộ hóa nói trên để đảm bảo an toàn luồng (trong số những thứ khác, ngăn việc đọc / ghi đồng thời), thì vấn đề nguyên tử sẽ được tranh luận. Nhưng chúng ta vẫn cần / muốn nó cho các kiểu dữ liệu đơn giản, nơi atomiccó giá trị thực. Câu hỏi hay!
Rob

Câu trả lời:


52

Còn rất sớm để cho rằng không có sẵn tài liệu cấp thấp, nhưng bạn có thể nghiên cứu từ quá trình lắp ráp. Hopper Disassembler là một công cụ tuyệt vời.

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

Sử dụng objc_storeStrongobjc_setProperty_atomiccho phi giải phẫu và nguyên tử tương ứng, trong đó

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

sử dụng swift_retaintừ libswift_stdlib_corevà dường như không có tích hợp an toàn luồng.

Chúng tôi có thể suy đoán rằng các từ khóa bổ sung (tương tự như @lazy) có thể được giới thiệu sau này.

Cập nhật 20/07/15 : theo blogpost này trên môi trường nhanh của singletons có thể làm cho một số trường hợp an toàn cho bạn, tức là:

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

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

}

Cập nhật 25/05/16 : Theo dõi đề xuất phát triển nhanh chóng https://github.com/apple/swift-evolution/blob/master/proposal/0030-property-behavior-decls.md - có vẻ như vậy sẽ có thể có @atomichành vi do chính bạn thực hiện.


Tôi cập nhật câu trả lời của tôi với một số thông tin gần đây, hy vọng nó giúp
Sash Zats

1
Xin chào, cảm ơn vì liên kết đến công cụ Hopper Disassembler. Trông gọn gàng.
C0D3

11

Swift không có cấu trúc ngôn ngữ nào xoay quanh vấn đề an toàn luồng. Giả định rằng bạn sẽ sử dụng các thư viện được cung cấp để thực hiện quản lý an toàn luồng của riêng bạn. Có một số lượng lớn các tùy chọn bạn có trong việc triển khai an toàn luồng bao gồm pthread mutexes, NSLock, và disp_sync dưới dạng cơ chế mutex. Xem bài đăng gần đây của Mike Ash về chủ đề: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html Vì vậy, câu trả lời trực tiếp cho câu hỏi của bạn về "Có thể Tôi đọc và ghi song song vào biến này một cách an toàn? " là Không.


7

Có lẽ còn sớm để trả lời câu hỏi này. Hiện tại, swift thiếu các công cụ sửa đổi quyền truy cập, vì vậy không có cách rõ ràng nào để thêm mã quản lý đồng thời xung quanh một getter / setter thuộc tính. Hơn nữa, Ngôn ngữ Swift dường như chưa có bất kỳ thông tin nào về đồng thời! (Nó cũng thiếu KVO, v.v.)

Tôi nghĩ rằng câu trả lời cho câu hỏi này sẽ trở nên rõ ràng trong các bản phát hành trong tương lai.


re: thiếu KVO, kiểm tra willSet, didSet- dường như là một bước đầu tiên trên đường
Sash Zats

1
willSet, didSet dành cho các thuộc tính luôn cần một bộ thiết lập tùy chỉnh vì chúng phải làm gì đó. Ví dụ: thuộc tính màu cần vẽ lại chế độ xem khi thuộc tính được thay đổi thành giá trị khác; điều đó hiện được thực hiện dễ dàng hơn bằng didSet.
gnasher729

vâng, đó là những gì tôi có nghĩa của "một bước đầu tiên" :) tôi cho rằng nó có thể là một dấu hiệu của tính năng là có sẵn nhưng không thực hiện đầy đủ chưa
Sash Zats

6

Chi tiết

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

Liên kết

Các loại được triển khai

Ý chính

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

Mẫu truy cập nguyên tử

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

Sử dụng

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

Kết quả

nhập mô tả hình ảnh ở đây


Một dự án mẫu trên github sẽ rất hay!
Klaas

1
Xin chào! Đây là mẫu đầy đủ. Sao chép Atomiclớp và chạy nó sử dụngAtomic().semaphoreSample()
Vasily Bodnarchuk

Vâng, tôi đã làm. Nghĩ rằng sẽ thật tuyệt, nếu nó là một dự án được cập nhật theo cú pháp mới nhất. Với Swift, cú pháp luôn thay đổi. Và câu trả lời của bạn cho đến nay là câu trả lời gần đây nhất :)
Klaas

1

Từ Swift 5.1, bạn có thể sử dụng trình bao bọc thuộc tính để tạo logic cụ thể cho các thuộc tính của mình. Đây là triển khai trình bao bọc nguyên tử:

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

Cách sử dụng:

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

0

Đây là trình bao bọc thuộc tính nguyên tử mà tôi sử dụng rộng rãi. Tôi đã biến cơ chế khóa thực sự thành một giao thức, vì vậy tôi có thể thử nghiệm với các cơ chế khác nhau. Tôi cố gắng Cột, DispatchQueuespthread_rwlock_t. Nó pthread_rwlock_tđược chọn vì nó có vẻ có chi phí thấp nhất và khả năng đảo ngược ưu tiên thấp hơn.

/// 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()
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.