为什么 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_enter
在 class
中运行良好,但在 struct
中运行不佳?
objc_sync_enter
/objc_sync_exit
函数采用对象实例并使用其标识(即内存中的地址)来分配和关联内存中的锁——并使用该锁来保护enter
和 exit
调用之间的代码。
但是,struct
s 不是对象,并且没有允许以这种方式使用它们的引用语义——它们甚至不能保证分配在内存中的稳定位置.然而,为了支持与 Objective-C 的互操作,struct
s 必须 在从 Objective-C 使用时具有一致的 object-like 表示,否则调用Objective-C 代码,例如 Any
实例中的 struct
可能会触发未定义的行为。
当 struct
以对象的形式传递给 Objective-C 时(例如,在 Any
或 AnyObject
内部),它被包裹在名为 __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_enter
。 objc_sync_enter
然后会将一个新锁与自动为您创建的临时对象值相关联...然后该对象将立即释放。这意味着两件事:
- 当你调用
objc_sync_exit
时,一个new对象会被创建并传入,但是运行时间没有与那个新对象关联的锁实例!此时可能会崩溃。
- 每次调用
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。)
我有下面的演示代码。
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_enter
在 class
中运行良好,但在 struct
中运行不佳?
objc_sync_enter
/objc_sync_exit
函数采用对象实例并使用其标识(即内存中的地址)来分配和关联内存中的锁——并使用该锁来保护enter
和 exit
调用之间的代码。
但是,struct
s 不是对象,并且没有允许以这种方式使用它们的引用语义——它们甚至不能保证分配在内存中的稳定位置.然而,为了支持与 Objective-C 的互操作,struct
s 必须 在从 Objective-C 使用时具有一致的 object-like 表示,否则调用Objective-C 代码,例如 Any
实例中的 struct
可能会触发未定义的行为。
当 struct
以对象的形式传递给 Objective-C 时(例如,在 Any
或 AnyObject
内部),它被包裹在名为 __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_enter
。 objc_sync_enter
然后会将一个新锁与自动为您创建的临时对象值相关联...然后该对象将立即释放。这意味着两件事:
- 当你调用
objc_sync_exit
时,一个new对象会被创建并传入,但是运行时间没有与那个新对象关联的锁实例!此时可能会崩溃。 - 每次调用
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。)