Links

30.锁|递归锁

一、 锁的分类

提到锁的分类一般都能说出两种:互斥锁自旋锁。这些年我一直是按上厕所理解的,印象挺深的。

1.1 互斥锁

  • 有人在用厕所了
    • 上锁
  • 来一个人就外面等着
    • 外面的也要排队
  • 里面的完事儿了
    • 解锁
  • 按排队的顺序下一个进去,继续循环
关键就是两点:互斥同步(顺序等待)

1.2 自旋锁

  • 有人在用厕所了
    • 上锁
  • 来一个人就外面等着
    • 但一个个都是暴脾气,不想排队,都不停在拽门,想抢先进去
  • 里面的完事儿了
    • 解锁
  • 这堆人里面就有一个抢进去了
    • 其他的继续不停的拽门
关键就是两点:互斥忙等(死循环)
其实锁还有一些更细分的类型,下面我们就来看看

二、 递归锁 & 非递归锁

2.1 多线程混乱

我们来看一个例子,下面打印输出会是什么样。
let queue = DispatchQueue.init(label: "RyukieQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
func testLog(times: Int) {
print("\(times) 号选手上完厕所了,线程: \(Thread.current)")
if times > 0 {
testLog(times: times - 1)
}
}
func demo() {
for _ in 0..<10 {
queue.async {
testLog(times: 10)
}
}
}
demo()
这里很简单,上厕所会十分的混乱。输出如下:
10 号选手上完厕所了,线程: <NSThread: 0x10073cab0>{number = 2, name = (null)}
9 号选手上完厕所了,线程: <NSThread: 0x10073cab0>{number = 2, name = (null)}
10 号选手上完厕所了,线程: <NSThread: 0x100536080>{number = 3, name = (null)}
9 号选手上完厕所了,线程: <NSThread: 0x100536080>{number = 3, name = (null)}
8 号选手上完厕所了,线程: <NSThread: 0x100536080>{number = 3, name = (null)}
7 号选手上完厕所了,线程: <NSThread: 0x100536080>{number = 3, name = (null)}
6 号选手上完厕所了,线程: <NSThread: 0x100536080>{number = 3, name = (null)}
...

2.2 NSLock

let lock1 = NSLock()
func demo1() {
for _ in 0..<10 {
queue.async {
lock1.lock()
testLog(times: 10)
lock1.unlock()
}
}
}
加了锁后,大家都在很好的排队上厕所了,完美。
10 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
9 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
8 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
7 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
6 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
5 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
4 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
3 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
2 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
1 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
0 号选手上完厕所了,线程: <NSThread: 0x104845250>{number = 2, name = (null)}
...

2.3 NSLock 递归加锁的问题

这次我们换个地方加锁,也和我们一般的业务场景相似,大多情况是在最上层具体业务场景加锁。
let lock1 = NSLock()
func testLog1(times: Int) {
lock1.lock() // 不断递归加锁
print("\(times) 号选手上完厕所了,线程: \(Thread.current)")
if times > 0 {
testLog1(times: times - 1)
}
lock1.unlock()
}
func demo11() {
for _ in 0..<10 {
queue.async {
testLog1(times: 10)
}
}
}
demo11()
这时的输出就有些不一样了,只输出了这一行。因为 NSLock 是非递归锁,但是这里又在循环递归加锁,这里就产生了类似死锁的现象。
10 号选手上完厕所了,线程: <NSThread: 0x104031980>{number = 2, name = (null)}

2.4 NSRecursiveLock

NSRecursiveLock 是可递归的
let lock2 = NSRecursiveLock()
func demo22() {
for _ in 0..<10 {
queue.async {
testLog2(times: 10)
}
}
}
func testLog2(times: Int) {
lock2.lock()
print("\(times) 号选手上完厕所了,线程: \(Thread.current)")
if times > 0 {
testLog2(times: times - 1)
}
lock2.unlock()
}
demo22()
这时厕所又恢复了秩序。
10 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
9 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
8 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
7 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
6 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
5 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
4 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
3 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
2 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
1 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
0 号选手上完厕所了,线程: <NSThread: 0x101012b50>{number = 2, name = (null)}
10 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
9 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
8 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
7 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
6 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
5 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
4 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
3 号选手上完厕所了,线程: <NSThread: 0x1010130d0>{number = 3, name = (null)}
...

多线程递归加锁的问题

按照官方文档的说明:
  • NSRecursiveLock 类定义了一个可以被同一个线程多次获取而不会导致线程死锁的锁。 递归锁会跟踪它被成功获取的次数。 每次成功获取锁都必须通过相应的解锁调用来平衡。 只有当所有的 lock 和 unlock 调用都平衡时,锁才真正被释放,以便其他线程可以获取它。
  • 顾名思义,这种类型的锁通常用于递归函数内部,以防止递归阻塞线程。 您可以类似地在非递归情况下使用它来调用语义要求它们也获取锁的函数。 这是一个通过递归获取锁的简单递归函数的示例。 如果您没有为此代码使用 NSRecursiveLock 对象,则再次调用该函数时线程将死锁。
  • 注意
    • 因为在所有锁调用与解锁调用平衡之前不会释放递归锁,所以您应该仔细权衡使用性能锁的决定与潜在的性能影响。 长时间持有任何锁可能会导致其他线程阻塞,直到递归完成。
总结一下: NSRecursiveLock 不支持多线程递归重复加锁。这也是平时更多使用 @synchronized 的一个原因,更全面,支持多线程递归加锁。
但是你可能发现了,我这里的代码使用没有任何问题。我怀疑是不是 Swift 下的 NSRecursiveLock 有所不同引起的。我再写用个 OC 看看。
1
果然是会崩溃。
可见 Swift 下有所不同,至于为什么我还没有找到线索,如果你知道的话欢迎交流。

参考