NSLock 在 属性 setter 中的使用
Usage of NSLock in property setter
比方说,我想让一个变量成为线程安全的。执行此操作的最常见方法之一:
var value: A {
get { return queue.sync { self._value } }
set { queue.sync { self._value = newValue } }
}
但是,this property is not completely thread safe 如果我们更改值,如下例所示:
Class.value += 1
所以我的问题是:按照同样的原理使用NSLock 也不是完全线程安全的吗?
var value: A {
get {
lock.lock()
defer { lock.unlock() }
return self._value
}
set {
lock.lock()
defer { lock.unlock() }
self._value = newValue
}
}
这很有趣,我是第一次了解这个。
第一段代码中的问题是:
object.value += 1
与
具有相同的语义
object.value = object.value + 1
我们可以进一步扩展为:
let originalValue = queue.sync { object._value }
let newValue = origiinalValue + 1
queue.sync { self._value = newValue }
展开它可以清楚地看到 getter 和 setter 的同步工作正常,但它们不是整体同步的。上面代码中间的上下文切换可能会导致 _value
被另一个线程改变,而不 newValue
反映更改。
使用锁也会有完全相同的问题。它将扩展为:
lock.lock()
let originalValue = object._value
lock.unlock()
let newValue = originalValue + 1
lock.lock()
object._value = newValue
lock.unlock()
您可以通过使用一些日志记录语句检测您的代码来亲眼看到这一点,这些语句表明锁没有完全覆盖变更:
class C {
var lock = NSLock()
var _value: Int
var value: Int {
get {
print("value.get start")
print("lock.lock()")
lock.lock()
defer {
print("lock.unlock()")
lock.unlock()
print("value.get end")
}
print("getting self._value")
return self._value
}
set {
print("\n\n\nvalue.set start")
lock.lock()
print("lock.lock()")
defer {
print("lock.unlock()")
lock.unlock()
print("value.set end")
}
print("setting self._value")
self._value = newValue
}
}
init(_ value: Int) { self._value = value }
}
let object = C(0)
object.value += 1
在回答您的问题时,锁定方法与 GCD 方法存在完全相同的问题。原子访问器方法根本不足以确保更广泛的线程安全。
问题是,正如其他地方所讨论的,无害的 +=
运算符正在通过 getter 检索值,递增该值,并通过 setter 存储该新值.为了实现线程安全,整个过程需要包装在一个同步机制中。你想要一个原子增量操作,你会写一个方法来做到这一点。
所以,以你的 NSLock
为例,我可能会将同步逻辑移到它自己的方法中,例如:
class Foo<T> {
private let lock = NSLock()
private var _value: T
init(value: T) {
_value = value
}
var value: T {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
}
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
但是如果你想要一个操作以线程安全的方式增加值,你会写一个方法来做到这一点,例如:
extension Foo where T: Numeric {
func increment(by increment: T) {
lock.synchronized {
_value += increment
}
}
}
然后,而不是这种非线程安全的尝试:
foo.value += 1
您可以改为使用以下线程安全格式:
foo.increment(by: 1)
无论您使用何种同步机制(例如,锁、GCD 串行队列、reader-writer 模式),这种将增量过程包装在同步整个操作的方法中的模式都适用, os_unfair_lock
, 等等).
就其价值而言,Swift 5.5 actor
模式(在 SE-0306 中概述)将此模式正式化。考虑:
actor Bar<T> {
var value: T
init(value: T) {
self.value = value
}
}
extension Bar where T: Numeric {
func increment(by increment: T) {
value += increment
}
}
此处,increment
方法自动成为“参与者隔离”方法(即,它将同步),但 actor
将控制与 setter 的交互,因为它属性,即如果您尝试从 class 外部设置 value
,您将收到错误消息:
Actor-isolated property 'value' can only be mutated from inside the actor
比方说,我想让一个变量成为线程安全的。执行此操作的最常见方法之一:
var value: A {
get { return queue.sync { self._value } }
set { queue.sync { self._value = newValue } }
}
但是,this property is not completely thread safe 如果我们更改值,如下例所示:
Class.value += 1
所以我的问题是:按照同样的原理使用NSLock 也不是完全线程安全的吗?
var value: A {
get {
lock.lock()
defer { lock.unlock() }
return self._value
}
set {
lock.lock()
defer { lock.unlock() }
self._value = newValue
}
}
这很有趣,我是第一次了解这个。
第一段代码中的问题是:
object.value += 1
与
具有相同的语义object.value = object.value + 1
我们可以进一步扩展为:
let originalValue = queue.sync { object._value }
let newValue = origiinalValue + 1
queue.sync { self._value = newValue }
展开它可以清楚地看到 getter 和 setter 的同步工作正常,但它们不是整体同步的。上面代码中间的上下文切换可能会导致 _value
被另一个线程改变,而不 newValue
反映更改。
使用锁也会有完全相同的问题。它将扩展为:
lock.lock()
let originalValue = object._value
lock.unlock()
let newValue = originalValue + 1
lock.lock()
object._value = newValue
lock.unlock()
您可以通过使用一些日志记录语句检测您的代码来亲眼看到这一点,这些语句表明锁没有完全覆盖变更:
class C {
var lock = NSLock()
var _value: Int
var value: Int {
get {
print("value.get start")
print("lock.lock()")
lock.lock()
defer {
print("lock.unlock()")
lock.unlock()
print("value.get end")
}
print("getting self._value")
return self._value
}
set {
print("\n\n\nvalue.set start")
lock.lock()
print("lock.lock()")
defer {
print("lock.unlock()")
lock.unlock()
print("value.set end")
}
print("setting self._value")
self._value = newValue
}
}
init(_ value: Int) { self._value = value }
}
let object = C(0)
object.value += 1
在回答您的问题时,锁定方法与 GCD 方法存在完全相同的问题。原子访问器方法根本不足以确保更广泛的线程安全。
问题是,正如其他地方所讨论的,无害的 +=
运算符正在通过 getter 检索值,递增该值,并通过 setter 存储该新值.为了实现线程安全,整个过程需要包装在一个同步机制中。你想要一个原子增量操作,你会写一个方法来做到这一点。
所以,以你的 NSLock
为例,我可能会将同步逻辑移到它自己的方法中,例如:
class Foo<T> {
private let lock = NSLock()
private var _value: T
init(value: T) {
_value = value
}
var value: T {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
}
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
但是如果你想要一个操作以线程安全的方式增加值,你会写一个方法来做到这一点,例如:
extension Foo where T: Numeric {
func increment(by increment: T) {
lock.synchronized {
_value += increment
}
}
}
然后,而不是这种非线程安全的尝试:
foo.value += 1
您可以改为使用以下线程安全格式:
foo.increment(by: 1)
无论您使用何种同步机制(例如,锁、GCD 串行队列、reader-writer 模式),这种将增量过程包装在同步整个操作的方法中的模式都适用, os_unfair_lock
, 等等).
就其价值而言,Swift 5.5 actor
模式(在 SE-0306 中概述)将此模式正式化。考虑:
actor Bar<T> {
var value: T
init(value: T) {
self.value = value
}
}
extension Bar where T: Numeric {
func increment(by increment: T) {
value += increment
}
}
此处,increment
方法自动成为“参与者隔离”方法(即,它将同步),但 actor
将控制与 setter 的交互,因为它属性,即如果您尝试从 class 外部设置 value
,您将收到错误消息:
Actor-isolated property 'value' can only be mutated from inside the actor