使用队列和信号量进行并发和属性包装?

时间:2019-10-03 02:08:23

标签: swift concurrency grand-central-dispatch semaphore

我正在尝试创建一个线程安全的属性包装器。我只能认为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
   }
}

如何对其进行适当的测试和衡量,以查看这两种实现之间的差异以及它们是否有效?

1 个答案:

答案 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次性能测试,结果将被捕获)。单元测试报告):

    enter image description here

    因此,创建一个具有单元测试目标的项目(或根据需要向现有项目中添加一个单元测试目标),然后创建单元测试,并使用 command + u执行它们

  • 如果您为目标编辑方案,则可以选择随机化测试顺序,以确保测试的执行顺序不会影响性能:

    enter image description here

    我还将使测试目标使用发布版本,以确保您正在测试优化的版本。

这是使用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()
    }
}