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

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

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

201

您可以使用GCD。它比@synchronized更繁琐,但可以作为替代品:

let serialQueue = DispatchQueue(label: "com.test.mySerialQueue")
serialQueue.sync {
    // code
}

12
这很好,但缺乏与 @synchronized 相同的重新进入功能。 - Michael Waterfall
10
采用这种方法时需要小心,你的代码块可能会在其他线程上执行。API文档中指出:“为优化性能,当可能时此函数会在当前线程上执行代码块。” - bio
23
Matt Gallagher写的这篇文章非常好:http://www.cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html。 - wuf810
81
不,错了。尝试不是很成功。为什么呢?重要阅读材料(综合比较替代方案,注意事项)和来自Matt Gallagher的出色实用框架,请参见此处:https://www.cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @wuf810首先提到了这个(HT),但是低估了这篇文章的好处。所有人都应该阅读。(请最少投票以使其初始可见,但不要过多。) - t0rst
5
有人能否澄清为什么这个答案会导致死锁?Matt Gallagher的文章清楚地说明了这比synchronized更慢,但是为什么会导致死锁?@TomKraina @bio @t0rst - Bill
显示剩余12条评论

193

我自己曾经也在寻找这个功能,但得出结论:目前Swift内部还没有原生的构造函数能够实现这一点。

不过,基于一些Matt Bridges和其他人编写的代码,我编写了这个小的辅助函数。

func synced(_ lock: Any, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

使用方法非常简单明了

synced(self) {
    println("This is a synchronized closure")
}

关于这个问题,我发现了一个问题。在这里将数组作为锁参数传递似乎会导致一个非常晦涩的编译器错误。不过除此之外,它似乎按预期工作。

Bitcast requires both operands to be pointer or neither
  %26 = bitcast i64 %25 to %objc_object*, !dbg !378
LLVM ERROR: Broken function found, compilation aborted!

17
这很有用,能保留 @synchronized 块的语法,但请注意,它与 Objective-C 中的真正内置块语句 @synchronized 并不完全相同,因为 returnbreak 语句不能像普通语句那样跳出包围函数/循环。 - newacct
3
可能的错误原因是数组被传递为值而不是引用。 - James Alvarez
13
也许这是一个使用新的 defer 关键字的好地方,以确保即使 closure 抛出异常,也会调用 objc_sync_exit - devios1
5
根据所链接的文章,称这个答案“有缺陷”是不正确的。文章说这种方法“比理想情况下略慢一些”,并且“只限于苹果平台”。这远远不能说明它“有缺陷”。请注意,不要改变原文的意思。 - RenniePet
2
这篇非常有趣的文章解释了objc_sync_xxx的一个陷阱:https://straypixels.net/swift-dictionary-locking/ - Mike Taverne
显示剩余6条评论

167
我喜欢并使用这里许多的回答,所以你可以选择最适合你的方法。话虽如此,当我需要类似于objective-c的@synchronized的东西时,我更喜欢使用在swift 2中引入的defer语句。
{ 
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    //
    // code of critical section goes here
    //

} // <-- lock released when this block is exited

这种方法的好处是,你的关键部分可以以任何所需的方式退出包含块(例如returnbreakcontinuethrow),而“在延迟语句中的语句将被执行,无论程序控制如何转移。”1


1
我认为这可能是在这里提供的最优雅的解决方案。感谢您的反馈。 - Scott D
3
lock是什么?lock如何初始化? - Van Du Tran
6
lock 是任何 Objective-C 对象。 - ɲeuroburɳ
2
太好了!当 Swift 1 推出时,我写了一些锁辅助方法,但已经有一段时间没有重新审视过它们了。完全忘记了 defer;这才是正确的方式! - Randy
1
很棒的答案。值得一提的是,可以使用 do { ... } 来定义一段代码块,因此 do { obj_sync_enter(lock); defer { obj_sync_exit(lock); }; ...code... } 实现了与 @synchronized{ ...code... } 相同的效果。 - Gibezynu Nu
显示剩余3条评论

89

你可以使用 objc_sync_enter(obj: AnyObject?)objc_sync_exit(obj: AnyObject?) 将语句夹在中间。@synchronized 关键字在幕后使用这些方法。即:

objc_sync_enter(self)
... synchronized code ...
objc_sync_exit(self)

3
这是否会被苹果视为使用私有API? - Drux
2
不,objc_sync_enterobjc_sync_exit是在Objc-sync.h中定义的方法,而且是开源的:http://opensource.apple.com/source/objc4/objc4-371.2/runtime/objc-sync.h - bontoJR
如果多个线程尝试访问同一资源,会发生什么情况?第二个线程会等待、重试还是崩溃? - TruMan1
补充一下@bontoJR所说的,objc_sync_enter(…)objc_sync_exit(…)是由iOS/macOS等API提供的公共头文件_(看起来它们在路径usr/include/objc/objc-sync.h中的….sdk内)。找出某个东西是否为公共API最简单的方法是(在Xcode中)输入函数名称(例如objc_sync_enter();对于C函数,不需要指定参数),然后尝试使用命令单击它。如果它向您显示该API的头文件,则说明您做得很好(因为如果它不是公共的,您将无法看到头文件)_。 - Slipp D. Thompson

78

Objective-C中@synchronized指令的类比可以具有任意返回类型,并在Swift中具有良好的rethrows行为。

// Swift 3
func synchronized<T>(_ lock: AnyObject, _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

使用defer语句可以直接返回一个值,而不需要引入临时变量。
在Swift 2中,添加@noescape属性到闭包中以允许更多的优化:
// Swift 2
func synchronized<T>(lock: AnyObject, @noescape _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

根据 GNewc [1](我喜欢任意返回类型)和 Tod Cunningham [2](我喜欢defer)的答案。


Xcode告诉我,在Swift 3中,@noescape现在是默认的,并且已经被弃用。 - RenniePet
没错,这个答案中的代码是针对Swift 2的,需要进行一些适应以适用于Swift 3。我会在有时间的时候进行更新。 - werediver
1
你能解释一下用法吗?最好附上一个例子。提前感谢!在我的情况下,我有一个Set需要同步,因为我在DispatchQueue中操作它的内容。 - sancho
@sancho,我更喜欢让这篇文章简洁明了。你似乎在询问关于并发编程的一般性指南,这是一个广泛的问题。请尝试将其作为一个单独的问题提出来! - werediver

44

SWIFT 4

在Swift 4中,你可以使用GCD的调度队列来锁定资源。

class MyObject {
    private var internalState: Int = 0
    private let internalQueue: DispatchQueue = DispatchQueue(label:"LockingQueue") // Serial by default

    var state: Int {
        get {
            return internalQueue.sync { internalState }
        }

        set (newState) {
            internalQueue.sync { internalState = newState }
        }
    }
} 

这似乎无法在XCode8.1中工作。.serial似乎不可用。但是.concurrent是可用的。 :/ - Travis Griggs
2
默认值为.serial。 - Duncan Groenewald
2
请注意,此模式不能很好地防范大多数常见的多线程问题。例如,如果您同时运行 myObject.state = myObject.state + 1,它将不会计算总操作次数,而是产生一个不确定的值。为了解决这个问题,调用代码应该被包装在一个串行队列中,以便读取和写入都是原子性的。当然,Obj-c 的 @synchronized 也有同样的问题,因此从这个意义上说,您的实现是正确的。 - Berik
1
是的,myObject.state += 1 是一个读取和写入操作的组合。其他线程仍然可以在其中设置/写入值。根据 https://www.objc.io/blog/2018/12/18/atomic-variables/,更容易在同步块/闭包中运行 set 而不是在变量本身下运行。 - CyberMew

29

在现代的Swift 5中,具备返回能力:

/**
Makes sure no other thread reenters the closure before the one running has not returned
*/
@discardableResult
public func synchronized<T>(_ lock: AnyObject, closure:() -> T) -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    return closure()
}

使用以下方式来利用返回值的功能:

let returnedValue = synchronized(self) { 
     // Your code here
     return yourCode()
}

或者像这样的其他方式:
synchronized(self) { 
     // Your code here
    yourCode()
}

3
这是正确的答案而不是被接受并高投票的那个(它依赖于“最大公约数”)。“Thread”似乎基本上没有人使用或理解如何使用。我非常满意它 - 而“最大公约数”则充满了陷阱和限制。 - WestCoastProjects
1
正确的答案需要使用递归锁,就像objc_sync_enter一样。我更喜欢将lock参数隐藏在私有的let或iVar中,而不是使用self,除非它需要发布以允许其他人进行同步。这是一个非常罕见的情况,但如果发生这种情况,使用objc_sync_enter可以允许Swift和Objective-C之间的协作。此答案还允许返回值。出于这些原因,我选择在我的项目中使用此答案。 - Rik Renich

23

借鉴Bryan McLemore的回答,我扩展了它以支持Swift 2.0 defer能力中抛出异常时的安全处理。

func synchronized( lock:AnyObject, block:() throws -> Void ) rethrows
{
    objc_sync_enter(lock)
    defer {
        objc_sync_exit(lock)
    }

    try block()
}

最好使用 rethrows 来简化对于不会抛出错误的闭包的使用(无需使用 try),详见我的回答 - werediver

23
为添加返回功能,您可以这样做:
func synchronize<T>(lockObj: AnyObject!, closure: ()->T) -> T
{
  objc_sync_enter(lockObj)
  var retVal: T = closure()
  objc_sync_exit(lockObj)
  return retVal
}

随后,您可以使用以下方式调用它:
func importantMethod(...) -> Bool {
  return synchronize(self) {
    if(feelLikeReturningTrue) { return true }
    // do other things
    if(feelLikeReturningTrueNow) { return true }
    // more things
    return whatIFeelLike ? true : false
  }
}

10
在2018 WWDC的“理解崩溃和崩溃日志”会议414中,他们展示了使用带有同步调用的DispatchQueues的以下方式。
在Swift 4中应该是类似以下内容:
class ImageCache {
    private let queue = DispatchQueue(label: "sync queue")
    private var storage: [String: UIImage] = [:]
    public subscript(key: String) -> UIImage? {
        get {
          return queue.sync {
            return storage[key]
          }
        }
        set {
          queue.sync {
            storage[key] = newValue
          }
        }
    }
}

你可以使用带有屏障的并发队列来加快读取速度。同步和异步读取同时进行,写入新值时会等待之前的操作完成。

class ImageCache {
    private let queue = DispatchQueue(label: "with barriers", attributes: .concurrent)
    private var storage: [String: UIImage] = [:]

    func get(_ key: String) -> UIImage? {
        return queue.sync { [weak self] in
            guard let self = self else { return nil }
            return self.storage[key]
        }
    }

    func set(_ image: UIImage, for key: String) {
        queue.async(flags: .barrier) { [weak self] in
            guard let self = self else { return }
            self.storage[key] = image
        }
    }
}

你可能不需要使用同步阻塞读取并减慢队列。你只需将同步用于串行写入即可。 - Basheer_CAD

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