在这种情况下我需要某种显式同步吗?
class A {
let val: Int;
init(_ newVal: Int) {
val = newVal
}
}
public class B {
var a: A? = nil
public func setA() { a = A(0) }
public func hasA() -> Bool { return a != nil }
}
类B中还有另一个方法:
public func resetA() {
guard hasA() else { return }
a = A(1)
}
setA()
和resetA()
可以从任何线程以任何顺序调用。
我明白可能有一个竞争条件,如果并发一个线程调用setA()
和另一个线程调用resetA()
,结果不确定:val
将是0
,或1
,但我不在乎:无论如何,hasA()
将返回真,不会吗?
如果A答案会改变吗?是结构体而不是class
?简而言之,不,属性访问器不是原子的。参见WWDC 2016视频《Swift 3中使用GCD并发编程》,该视频讨论了Swift语言中原子/同步的缺失。(这是一个关于GCD的演讲,所以当他们随后深入到同步方法时,他们关注的是GCD方法,但任何同步方法都是可以的。)苹果在自己的代码中使用了各种不同的同步方法。例如,在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
的初始化)都在一个单一的同步步骤中。这是这个问题的一个很好的一般化解。并且它可以防止逻辑竞争。
上面的例子太抽象了,很难理解。让我们考虑一个实际的例子,一个苹果的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
}
}
}
}
举几个例子。希望它能说明这个想法。