我正在尝试创建一个线程安全的属性包装器。我只能认为GCD队列和信号量是最快捷,最可靠的方法。信号量是不是性能更高(如果的确是这样),还是出于并发性而使用另一个参数?
以下是原子属性包装器的两种变体:
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let queue = DispatchQueue(label: "Atomic serial queue")
var wrappedValue: Value {
get { queue.sync { value } }
set { queue.sync { value = newValue } }
}
init(wrappedValue value: Value) {
self.value = value
}
}
@propertyWrapper
struct Atomic2<Value> {
private var value: Value
private var semaphore = DispatchSemaphore(value: 1)
var wrappedValue: Value {
get {
semaphore.wait()
let temp = value
semaphore.signal()
return temp
}
set {
semaphore.wait()
value = newValue
semaphore.signal()
}
}
init(wrappedValue value: Value) {
self.value = value
}
}
struct MyStruct {
@Atomic var counter = 0
@Atomic2 var counter2 = 0
}
func test() {
var myStruct = MyStruct()
DispatchQueue.concurrentPerform(iterations: 1000) {
myStruct.counter += $0
myStruct.counter2 += $0
}
}
如何对其进行适当的测试和衡量,以查看这两种实现之间的差异以及它们是否有效?
答案 0 :(得分:4)
FWIW,另一种选择是带有并发队列的读写器模式,其中读取是同步完成的,但是相对于其他读取可以同时运行,但是写入是异步完成的,但是有一个障碍(即,相对于其他并发)其他任何读写操作):
@propertyWrapper
class Atomic<Value> {
private var value: Value
private let queue = DispatchQueue(label: "com.domain.app.atomic", attributes: .concurrent)
var wrappedValue: Value {
get { queue.sync { value } }
set { queue.async(flags: .barrier) { self.value = newValue } }
}
init(wrappedValue value: Value) {
self.value = value
}
}
还有一个是锁:
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private var lock = NSLock()
var wrappedValue: Value {
get { lock.synchronized { value } }
set { lock.synchronized { value = newValue } }
}
init(wrappedValue value: Value) {
self.value = value
}
}
其中
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
我们应该认识到,尽管这些以及您的提供了原子性,但它不会提供线程安全的交互。
考虑这个简单的实验,我们将整数增加一百万次:
@Atomic var foo = 0
func threadSafetyExperiment() {
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
self.foo += 1
}
print(self.foo)
}
}
您希望foo
等于1,000,000,但不会如此。这是因为“获取并增加并保存值”的整个交互过程都需要封装在一个同步机制中。
因此,您将返回到非属性包装类解决方案,例如
class Synchronized<Value> {
private var _value: Value
private let lock = NSLock()
init(_ value: Value) {
self._value = value
}
var value: Value {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
func synchronized(block: (inout Value) -> Void) {
lock.synchronized {
block(&_value)
}
}
}
然后工作正常:
var foo = Synchronized<Int>(0)
func threadSafetyExperiment() {
DispatchQueue.global().async {
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
self.foo.synchronized { value in
value += 1
}
}
print(self.foo.value)
}
}
如何对其进行适当的测试和衡量,以查看这两种实现之间的差异以及它们是否有效?
一些想法:
我建议进行1000次以上的迭代。您想要进行足够的迭代,以秒为单位测量结果,而不是毫秒。我个人使用了一百万次迭代。
单元测试框架非常适合测试正确性以及使用measure
方法测量性能(该方法对每个单元测试重复进行10次性能测试,结果将被捕获)。单元测试报告):
因此,创建一个具有单元测试目标的项目(或根据需要向现有项目中添加一个单元测试目标),然后创建单元测试,并使用 command + u执行它们。
如果您为目标编辑方案,则可以选择随机化测试顺序,以确保测试的执行顺序不会影响性能:
我还将使测试目标使用发布版本,以确保您正在测试优化的版本。
这是使用GCD串行队列,并发队列,锁,不公平锁,信号灯的各种不同同步的示例:
class SynchronizedSerial<Value> {
private var _value: Value
private let queue = DispatchQueue(label: "com.domain.app.atomic")
required init(_ value: Value) {
self._value = value
}
var value: Value {
get { queue.sync { _value } }
set { queue.async { self._value = newValue } }
}
func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
try queue.sync {
try block(&_value)
}
}
func writer(block: @escaping (inout Value) -> Void) -> Void {
queue.async {
block(&self._value)
}
}
}
class SynchronizedReaderWriter<Value> {
private var _value: Value
private let queue = DispatchQueue(label: "com.domain.app.atomic", attributes: .concurrent)
required init(_ value: Value) {
self._value = value
}
var value: Value {
get { queue.sync { _value } }
set { queue.async(flags: .barrier) { self._value = newValue } }
}
func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
try queue.sync(flags: .barrier) {
try block(&_value)
}
}
func reader<T>(block: (Value) throws -> T) rethrows -> T {
try queue.sync {
try block(_value)
}
}
func writer(block: @escaping (inout Value) -> Void) -> Void {
queue.async(flags: .barrier) {
block(&self._value)
}
}
}
struct SynchronizedLock<Value> {
private var _value: Value
private let lock = NSLock()
init(_ value: Value) {
self._value = value
}
var value: Value {
get { lock.synchronized { _value } }
set { lock.synchronized { _value = newValue } }
}
mutating func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
try lock.synchronized {
try block(&_value)
}
}
}
/// Unfair lock synchronization
///
/// - Warning: The documentation warns us: “In general, higher level synchronization primitives such as those provided by the pthread or dispatch subsystems should be preferred.”</quote>
class SynchronizedUnfairLock<Value> {
private var _value: Value
private var lock = os_unfair_lock()
required init(_ value: Value) {
self._value = value
}
var value: Value {
get { synchronized { $0 } }
set { synchronized { $0 = newValue } }
}
func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
return try block(&_value)
}
}
struct SynchronizedSemaphore<Value> {
private var _value: Value
private let semaphore = DispatchSemaphore(value: 1)
init(_ value: Value) {
self._value = value
}
var value: Value {
get { semaphore.waitAndSignal { _value } }
set { semaphore.waitAndSignal { _value = newValue } }
}
mutating func synchronized<T>(block: (inout Value) throws -> T) rethrows -> T {
try semaphore.waitAndSignal {
try block(&_value)
}
}
}
extension NSLocking {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
extension DispatchSemaphore {
func waitAndSignal<T>(block: () throws -> T) rethrows -> T {
wait()
defer { signal() }
return try block()
}
}