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