什么是Swift中等同于Objective-C的"@synchronized"的方法?

267
我查阅了Swift书籍,但未能找到@synchronized的Swift版本。在Swift中如何实现互斥?

1
我会使用调度栅栏。栅栏提供非常便宜的同步。dispatch_barrier_async()等。 - Frederick C. Lee
@FrederickC.Lee,如果您需要同步写入,例如在为removeFirst()创建包装器时,该怎么办? - ScottyBlades
24个回答

10

尝试使用:NSRecursiveLock

一种锁,同一线程可以多次获取而不会导致死锁。

let lock = NSRecursiveLock()

func f() {
    lock.lock()
    //Your Code
    lock.unlock()
}

func f2() {
    lock.lock()
    defer {
        lock.unlock()
    }
    //Your Code
}

Objective-C的同步功能支持递归和可重入代码。一个线程可以以递归方式多次使用单个信号量;直到该线程释放使用它获取的所有锁,其他线程才能使用它;也就是说,每个@synchronized()块正常退出或通过异常退出。来源

8

Swift 3

这段代码具有可重入性,并可以与异步函数调用一起工作。在此代码中,调用 someAsyncFunc() 后,另一个串行队列上的函数闭包将被处理,但会被 semaphore.wait() 阻塞,直到 signal() 被调用。如果我没记错的话,不应该使用 internalQueue.sync,因为它会阻塞主线程。

let internalQueue = DispatchQueue(label: "serialQueue")
let semaphore = DispatchSemaphore(value: 1)

internalQueue.async {

    self.semaphore.wait()

    // Critical section

    someAsyncFunc() {

        // Do some work here

        self.semaphore.signal()
    }
}

如果没有错误处理,使用objc_sync_enter/objc_sync_exit并不是一个好主意。


什么错误处理?编译器不允许任何会抛出异常的东西。另一方面,如果不使用objc_sync_enter/exit,你就放弃了一些实质性的性能提升。 - gnasher729

6

在Swift4中使用NSLock

let lock = NSLock()
lock.lock()
if isRunning == true {
        print("Service IS running ==> please wait")
        return
} else {
    print("Service not running")
}
isRunning = true
lock.unlock()

警告 NSLock类使用POSIX线程来实现其锁定行为。当向一个NSLock对象发送解锁消息时,您必须确保该消息是从发送初始锁定消息的相同线程发送的。从不同的线程解锁锁可能会导致未定义的行为。


1
消除基于锁的代码 - Gobe

6

随着Swift并发性的出现,我们将使用actors(演员)

You can use tasks to break up your program into isolated, concurrent pieces. Tasks are isolated from each other, which is what makes it safe for them to run at the same time, but sometimes you need to share some information between tasks. Actors let you safely share information between concurrent code.

Like classes, actors are reference types, so the comparison of value types and reference types in Classes Are Reference Types applies to actors as well as classes. Unlike classes, actors allow only one task to access their mutable state at a time, which makes it safe for code in multiple tasks to interact with the same instance of an actor. For example, here’s an actor that records temperatures:

actor TemperatureLogger {
    let label: String
    var measurements: [Int]
    private(set) var max: Int

    init(label: String, measurement: Int) {
        self.label = label
        self.measurements = [measurement]
        self.max = measurement
    }
}

You introduce an actor with the actor keyword, followed by its definition in a pair of braces. The TemperatureLogger actor has properties that other code outside the actor can access, and restricts the max property so only code inside the actor can update the maximum value.

了解更多信息,请参见WWDC视频使用Swift actors保护可变状态


为了完整起见,历史上的替代方案包括:

  • GCD串行队列:这是一种简单的预并发方法,确保一次只有一个线程与共享资源交互。

  • 带有并发GCD队列的读写器模式:在读写器模式中,使用并发调度队列执行同步但并发的读取(但仅与其他读取并发),但通过障碍异步执行写入(强制写入不与该队列上的任何其他内容同时进行)。这可以比简单的GCD串行解决方案提供性能改进,但实际上,优势有限,并且需要额外的复杂性(例如,您必须小心线程爆炸场景)。在我看来,我倾向于避免使用此模式,要么坚持使用串行队列模式的简单性,要么在性能差异至关重要时使用完全不同的模式。

  • 锁:在我的Swift测试中,基于锁的同步化往往比两种GCD方法都快得多。锁有几种类型:

    • NSLock是一种不错的、相对高效的锁机制。
    • 在那些性能至关重要的情况下,我使用“unfair locks”,但是必须小心从Swift中使用它们(参见https://dev59.com/H7joa4cB1Zd3GeqPG-7v#66525671)。
    • 为了完整起见,还有递归锁。在我看来,我更喜欢简单的NSLock而不是NSRecursiveLock。递归锁容易被滥用,通常表示代码有问题。
    • 您可能会看到“自旋锁”的引用。多年前,它们曾被用于性能至关重要的情况下,但现在已被弃用,改用“unfair locks”。
  • 技术上,可以使用信号量进行同步,但它往往是所有替代方案中最慢的。

我在这里概述了一些我的基准测试结果

简而言之,现在我在当代代码库中使用actors,在简单场景非异步等待代码中使用GCD串行队列,在那些性能至关重要的罕见情况下使用锁。

不用说,我们经常尝试完全减少同步的数量。如果可以的话,我们经常使用值类型,其中每个线程都有自己的副本。在无法避免同步的情况下,我们尽可能减少这些同步的数量。


6

使用 Swift 的属性包装器,这就是我现在正在使用的:

@propertyWrapper public struct NCCSerialized<Wrapped> {
    private let queue = DispatchQueue(label: "com.nuclearcyborg.NCCSerialized_\(UUID().uuidString)")

    private var _wrappedValue: Wrapped
    public var wrappedValue: Wrapped {
        get { queue.sync { _wrappedValue } }
        set { queue.sync { _wrappedValue = newValue } }
    }

    public init(wrappedValue: Wrapped) {
        self._wrappedValue = wrappedValue
    }
}

那么你只需要这样做:

@NCCSerialized var foo: Int = 10

或者

@NCCSerialized var myData: [SomeStruct] = []

使用通常的方式访问变量。


1
我喜欢这个解决方案,但是对于使用@Decorating的成本很好奇,因为这样做会产生一个对用户隐藏的“DispatchQueue”的副作用。我在这个SO参考中找到了答案,放心了:https://dev59.com/eVsW5IYBdhLWcg3wbm0O#35022486 - AJ Venturella
属性包装器本身非常轻巧--只是一个结构体,因此是您可以创建的最轻量级的东西之一。感谢提供DispatchQueue链接。我一直想在队列同步包装与其他解决方案(以及无队列)之间进行一些性能测试,但还没有这样做。 - drewster

2

您可以创建属性包装器 Synchronised

下面的示例使用了NSLock。您可以使用任何您想要的同步方式,如 GCD、posix_locks 等。

@propertyWrapper public struct Synchronised<T> {
    private let lock = NSLock()

    private var _wrappedValue: T
    public var wrappedValue: T {
        get {
            lock.lock()
            defer {
                lock.unlock()
            }
            return _wrappedValue
        }
        set {
            lock.lock()
            defer {
                lock.unlock()
            }
            _wrappedValue = newValue
        }
    }

    public init(wrappedValue: T) {
        self._wrappedValue = wrappedValue
    }
}

@Synchronised var example: String = "testing"

基于@drewster的回答


2

我会提供一个Swift 5的实现,基于之前的回答。感谢大家!我发现返回值的方法也很有用,因此我提供了两种方法。

首先,这是一个简单的类:


import Foundation
class Sync {
public class func synced(_ lock: Any, closure: () -> ()) {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        closure()
    }
    public class func syncedReturn(_ lock: Any, closure: () -> (Any?)) -> Any? {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        return closure()
    }
}

如果需要返回值,请按以下方式使用它:

最初的回答:

然后如果需要返回值就这样使用它:
return Sync.syncedReturn(self, closure: {
    // some code here
    return "hello world"
})

或者:

Sync.synced(self, closure: {
    // do some work synchronously
})

尝试使用public class func synced<T>(_ lock: Any, closure: () -> T),它适用于void和任何其他类型。还有一些关于regrows的东西。 - hnh
@hnh,你所说的regrows是什么意思?如果你愿意分享一个使用类型<T>的通用方法的示例调用,那将有助于我更新答案——我喜欢你的想法。 - TheJeff
rethrows, not regrows, srz - hnh

1
总之,这里提供了更常见的方式,包括返回值或空值和抛出异常。
import Foundation

extension NSObject {


    func synchronized<T>(lockObj: AnyObject!, closure: () throws -> T) rethrows ->  T
    {
        objc_sync_enter(lockObj)
        defer {
            objc_sync_exit(lockObj)
        }

        return try closure()
    }


}

为什么在 sync_enter 之后而不是之前放置 defer {sync_exit}?在开发会议上,我听说 defer 应该放在函数内所有代码之前 :) - iTux
因为在执行 objc_sync_enter 后再执行 objc_sync_exit 是合理的。 - Victor Choy
但是如果你把它放在前面,它会在输入后退出,如果你把它放在作用域退出时,它会在退出时退出,我说得对吗? - iTux

1
什么情况。
final class SpinLock {
    private let lock = NSRecursiveLock()

    func sync<T>(action: () -> T) -> T {
        lock.lock()
        defer { lock.unlock() }
        return action()
    }
}

1

详情

Xcode 8.3.1,Swift 3.1

任务

从不同的线程(异步)中读写值。

代码

class AsyncObject<T>:CustomStringConvertible {
    private var _value: T
    public private(set) var dispatchQueueName: String
   
    let dispatchQueue: DispatchQueue
    
    init (value: T, dispatchQueueName: String) {
        _value = value
        self.dispatchQueueName = dispatchQueueName
        dispatchQueue = DispatchQueue(label: dispatchQueueName)
    }
    
    func setValue(with closure: @escaping (_ currentValue: T)->(T) ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                _self._value = closure(_self._value)
            }
        }
    }
    
    func getValue(with closure: @escaping (_ currentValue: T)->() ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                closure(_self._value)
            }
        }
    }
    
    
    var value: T {
        get {
            return dispatchQueue.sync { _value }
        }
        
        set (newValue) {
            dispatchQueue.sync { _value = newValue }
        }
    }

    var description: String {
        return "\(_value)"
    }
}

使用方法

print("Single read/write action")
// Use it when when you need to make single action
let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch0")
obj.value = 100
let x = obj.value
print(x)

print("Write action in block")
// Use it when when you need to make many action
obj.setValue{ (current) -> (Int) in
    let newValue = current*2
    print("previous: \(current), new: \(newValue)")
    return newValue
}

完整示例

扩展 DispatchGroup

extension DispatchGroup {
    
    class func loop(repeatNumber: Int, action: @escaping (_ index: Int)->(), completion: @escaping ()->()) {
        let group = DispatchGroup()
        for index in 0...repeatNumber {
            group.enter()
            DispatchQueue.global(qos: .utility).async {
                action(index)
                group.leave()
            }
        }
        
        group.notify(queue: DispatchQueue.global(qos: .userInitiated)) {
            completion()
        }
    }
}

类 ViewController
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        //sample1()
        sample2()
    }
    
    func sample1() {
        print("=================================================\nsample with variable")
        
        let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch1")
        
        DispatchGroup.loop(repeatNumber: 5, action: { index in
            obj.value = index
        }) {
            print("\(obj.value)")
        }
    }
    
    func sample2() {
        print("\n=================================================\nsample with array")
        let arr = AsyncObject<[Int]>(value: [], dispatchQueueName: "Dispatch2")
        DispatchGroup.loop(repeatNumber: 15, action: { index in
            arr.setValue{ (current) -> ([Int]) in
                var array = current
                array.append(index*index)
                print("index: \(index), value \(array[array.count-1])")
                return array
            }
        }) {
            print("\(arr.value)")
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接