Swift 5 中的引用分配是原子的吗?
is reference assignment atomic in Swift 5?
在这种情况下我需要某种显式同步吗?
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 }
}
class中还有一个方法B:
public func resetA() {
guard hasA() else { return }
a = A(1)
}
setA()
和 resetA()
可以从任何线程以任何顺序调用。
我知道可能存在竞争条件,如果一个线程同时调用 setA()
而另一个线程调用 resetA()
,结果不确定:val
要么是0
或 1
,但我不在乎:无论如何,hasA()
将 return 为真,不是吗?
如果 A 是 struct 而不是 class[=35=,答案会改变吗]?
简而言之,不,属性 访问器不是原子的。请参阅 WWDC 2016 视频 Concurrent Programming With GCD in Swift 3, which talks about the absence of atomic/synchronization native in the language. (This is a GCD talk, so when they subsequently dive into synchronization methods, they focus on GCD methods, but any synchronization method is fine.) Apple uses a variety of different synchronization methods in their own code. E.g. in ThreadSafeArrayStore
they use they use 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
}
}
}
}
举几个例子。希望它能说明这个想法。
在这种情况下我需要某种显式同步吗?
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 }
}
class中还有一个方法B:
public func resetA() {
guard hasA() else { return }
a = A(1)
}
setA()
和 resetA()
可以从任何线程以任何顺序调用。
我知道可能存在竞争条件,如果一个线程同时调用 setA()
而另一个线程调用 resetA()
,结果不确定:val
要么是0
或 1
,但我不在乎:无论如何,hasA()
将 return 为真,不是吗?
如果 A 是 struct 而不是 class[=35=,答案会改变吗]?
简而言之,不,属性 访问器不是原子的。请参阅 WWDC 2016 视频 Concurrent Programming With GCD in Swift 3, which talks about the absence of atomic/synchronization native in the language. (This is a GCD talk, so when they subsequently dive into synchronization methods, they focus on GCD methods, but any synchronization method is fine.) Apple uses a variety of different synchronization methods in their own code. E.g. in ThreadSafeArrayStore
they use they use 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
}
}
}
}
举几个例子。希望它能说明这个想法。