简而言之,不,属性访问器不是原子的。请参阅 WWDC 2016 视频Concurrent Programming With GCD in Swift 3,其中讨论了该语言中缺乏原子/同步本机。(这是一个 GCD 演讲,所以当他们随后深入研究同步方法时,他们专注于 GCD 方法,但任何同步方法都可以。)Apple 在自己的代码中使用了各种不同的同步方法。例如,ThreadSafeArrayStore
他们使用他们使用NSLock
)。
如果与锁同步,我可能会建议如下扩展:
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
Apple 在他们自己的代码中使用了这种模式,尽管他们碰巧称之为它withLock
而不是synchronized
. 但模式是一样的。
然后你可以这样做:
public class B {
private var lock = NSLock()
private var a: A? // make this private to prevent unsynchronized direct access to this property
public func setA() {
lock.synchronized {
a = A(0)
}
}
public func hasA() -> Bool {
lock.synchronized {
a != nil
}
}
public func resetA() {
lock.synchronized {
guard a != nil else { return }
a = A(1)
}
}
}
也许
public class B {
private var lock = NSLock()
private var _a: A?
public var a: A? {
get { lock.synchronized { _a } }
set { lock.synchronized { _a = newValue } }
}
public var hasA: Bool {
lock.synchronized { _a != nil }
}
public func resetA() {
lock.synchronized {
guard _a != nil else { return }
_a = A(1)
}
}
}
我承认在暴露时有些不安hasA
,因为它实际上会邀请应用程序开发人员这样写:
if !b.hasA {
b.a = ...
}
就防止同时访问内存而言,这很好,但是如果两个线程同时执行它会引入逻辑竞争,两个线程都恰好通过了!hasA
测试,并且它们都替换了值,最后一个获胜。
相反,我可能会为我们编写一个方法来执行此操作:
public class B {
private var lock = NSLock() // replacing os_unfair_lock_s()
private var _a: A? = nil // fixed, thanks to Rob
var a: A? {
get { lock.synchronized { _a } }
set { lock.synchronized { _a = newValue } }
}
public func withA(block: (inout A?) throws -> T) rethrows -> T {
try lock.synchronized {
try block(&_a)
}
}
}
这样你就可以做到:
b.withA { a in
if a == nil {
a = ...
}
}
这是线程安全的,因为我们让调用者在一个同步步骤中包装所有逻辑任务(检查是否a
是nil
,如果是,则初始化)。a
这是一个很好的通用解决方案。它可以防止逻辑竞赛。
现在上面的例子太抽象了,很难理解。所以让我们考虑一个实际的例子,Apple 的一个变体ThreadSafeArrayStore
:
public class ThreadSafeArrayStore<Value> {
private var underlying: [Value]
private let lock = NSLock()
public init(_ seed: [Value] = []) {
underlying = seed
}
public subscript(index: Int) -> Value {
get { lock.synchronized { underlying[index] } }
set { lock.synchronized { underlying[index] = newValue } }
}
public func get() -> [Value] {
lock.synchronized {
underlying
}
}
public func clear() {
lock.synchronized {
underlying = []
}
}
public func append(_ item: Value) {
lock.synchronized {
underlying.append(item)
}
}
public var count: Int {
lock.synchronized {
underlying.count
}
}
public var isEmpty: Bool {
lock.synchronized {
underlying.isEmpty
}
}
public func map<NewValue>(_ transform: (Value) throws -> NewValue) rethrows -> [NewValue] {
try lock.synchronized {
try underlying.map(transform)
}
}
public func compactMap<NewValue>(_ transform: (Value) throws -> NewValue?) rethrows -> [NewValue] {
try lock.synchronized {
try underlying.compactMap(transform)
}
}
}
这里我们有一个同步数组,我们定义一个接口以线程安全的方式与底层数组交互。
或者,如果您想要一个更简单的示例,请考虑使用线程安全对象来跟踪最高的项目是什么。我们不会有一个hasValue
布尔值,而是将它合并到我们的同步updateIfTaller
方法中:
public class Tallest {
private var _height: Float?
private let lock = NSLock()
var height: Float? {
lock.synchronized { _height }
}
func updateIfTaller(_ candidate: Float) {
lock.synchronized {
guard let tallest = _height else {
_height = candidate
return
}
if candidate > tallest {
_height = candidate
}
}
}
}
只是几个例子。希望它能说明这个想法。