为什么 objc_sync_enter 对 struct 不好用,而对 class 很好用?

What is the reason behind objc_sync_enter doesn't work well with struct, but works well with class?

我有下面的演示代码。

struct IdGenerator {
    private var lastId: Int64
    private var set: Set<Int64> = []
    
    init() {
        self.lastId = 0
    }
    
    mutating func nextId() -> Int64 {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        
        repeat {
            lastId = lastId + 1
        } while set.contains(lastId)

        precondition(lastId > 0)
        
        let (inserted, _) = set.insert(lastId)
        precondition(inserted)
        
        return lastId
    }
}

var idGenerator = IdGenerator()

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func click(_ sender: Any) {
        DispatchQueue.global(qos: .userInitiated).async {
            for i in 1...10000 {
                let id = idGenerator.nextId()
                print("i : \(id)")
            }
        }

        DispatchQueue.global(qos: .userInitiated).async {
            for j in 1...10000 {
                let id = idGenerator.nextId()
                print("j : \(id)")
            }
        }
    }
}

每当我执行 click 时,我都会遇到以下崩溃

Thread 5 Queue : com.apple.root.user-initiated-qos (concurrent)
#0  0x000000018f58b434 in _NativeSet.insertNew(_:at:isUnique:) ()
#1  0x000000018f598d10 in Set._Variant.insert(_:) ()
#2  0x00000001001fe31c in IdGenerator.nextId() at /Users/yccheok/Desktop/xxxx/xxxx/ViewController.swift:30
#3  0x00000001001fed8c in closure #2 in ViewController.click(_:) at /Users/yccheok/Desktop/xxxx/xxxx/ViewController.swift:56

不清楚为什么会发生崩溃。我的原始猜测是,在 struct 下,objc_sync_enter(self) 没有按预期工作。 2个线程同时访问Set就会出现这样的问题。


如果我将 struct 更改为 class,一切正常。

class IdGenerator {
    private var lastId: Int64
    private var set: Set<Int64> = []
    
    init() {
        self.lastId = 0
    }
    
    func nextId() -> Int64 {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        
        repeat {
            lastId = lastId + 1
        } while set.contains(lastId)

        precondition(lastId > 0)
        
        let (inserted, _) = set.insert(lastId)
        precondition(inserted)
        
        return lastId
    }
}

请问这是什么原因?为什么上面的 objc_sync_enterclass 中运行良好,但在 struct 中运行不佳?

objc_sync_enter/objc_sync_exit 函数采用对象实例并使用其标识(即内存中的地址)来分配和关联内存中的锁——并使用该锁来保护enterexit 调用之间的代码。

但是,structs 不是对象,并且没有允许以这种方式使用它们的引用语义——它们甚至不能保证分配在内存中的稳定位置.然而,为了支持与 Objective-C 的互操作,structs 必须 在从 Objective-C 使用时具有一致的 object-like 表示,否则调用Objective-C 代码,例如 Any 实例中的 struct 可能会触发未定义的行为。

struct 以对象的形式传递给 Objective-C 时(例如,在 AnyAnyObject 内部),它被包裹在名为 __SwiftValue 的私有 class 类型的 临时 对象。这允许它 看起来 像 Objective-C 的对象,并且在某些情况下,像对象一样使用,但重要的是,它 不是 一个long-lived,稳定对象

您可以通过以下代码查看:

struct Foo {}
let f = Foo()
print(f) // => Foo()
print(f as AnyObject) // => __SwiftValue

print(ObjectIdentifier(f as AnyObject)) // => ObjectIdentifier(0x0000600002595900)
print(ObjectIdentifier(f as AnyObject)) // => ObjectIdentifier(0x0000600002595c60)

指针会改变运行运行,但是你可以看到每次f作为AnyObject访问时,它都会有一个地址。

这意味着当您在 struct 上调用 objc_sync_enter 时,将创建一个新的 __SwiftValue 对象来包装您的 struct,并且 对象被传递给 objc_sync_enterobjc_sync_enter 然后会将一个新锁与自动为您创建的临时对象值相关联...然后该对象将立即释放。这意味着两件事:

  1. 当你调用objc_sync_exit时,一个new对象会被创建并传入,但是运行时间没有与那个新对象关联的锁实例!此时可能会崩溃。
  2. 每次调用objc_sync_enter,你都在创建一个新的、单独的锁...这意味着实际上根本没有同步:每个线程都是完全获得一把新锁。

不能保证这个新的指针实例 — 根据优化,对象 可能 存在足够长的时间以在 objc_sync_* 调用中重复使用,或者新对象可以被分配到与旧对象完全相同的位置......或者一个新对象可以被分配到一个不同结构曾经所在的地方,你不小心解锁了一个不同的线程......

所有这些意味着你绝对应该避免使用 objc_sync_enter/objc_sync_exit 作为 Swift 的锁定机制,并切换到 NSLock, an allocated os_unfair_lock 之类的东西,或者甚至 DispatchQueue,也就是 Swift 的 well-supported。 (实际上,objc_sync_* 函数是主要由 Obj-C 运行 时间使用的原语,可能应该是 un-exposed 到 Swift。)