Tidwall/Safe 互斥死锁
Tidwall/Safe mutex deadlock
我正在使用 https://github.com/tidwall/Safe ,一个 Swift 并发库,我想我发现了一个线程错误。 (我正在使用 IOS 12.3.1,iPhone Xs。Swift 4,我想;Xcode 10.2。)图书馆现在是只读的,所以我我正在尝试自己调试它。不过,这个错误真的很微妙,或者它是由我什至没有想到的东西引起的,因为我做了几乎与给定库相同的事情并且它工作正常,但是库本身死锁了。
下面是本不该死锁的测试代码:
private func testCompetingDeadlock() {
NSLog("start")
let c = Chan<Int32>()
let b = Chan<Int32>()
let COUNT = 1000
let wg = WaitGroup()
wg.add(1)
dispatch {
NSLog("receiver starting")
for i in 0..<(2*COUNT) {
Thread.sleep(forTimeInterval: 0.01)
let v = <-c
b <- v!
}
wg.done()
}
sleep(1)
wg.add(1)
dispatch {
NSLog("sender 1 starting")
for i in 0..<COUNT {
c <- 1
<-b
NSLog("1 : \(i)")
}
NSLog("1 done")
wg.done()
}
wg.add(1)
dispatch {
NSLog("sender 2 starting")
for i in 0..<COUNT {
c <- 2
<-b
NSLog("2 : \(i)")
}
NSLog("2 done")
wg.done()
}
wg.wait()
NSLog("Both done")
}
请注意 send
又名 <-
的底层实现是
internal func send(_ msg: T) {
NSLog("locking (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
cond.mutex.lock()
NSLog("locked (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
threadCount += 1
defer {
threadCount -= 1
NSLog("unlocking (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
cond.mutex.unlock()
NSLog("unlocked (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
}
if threadCount > 1 {
NSLog("threadCount is \(threadCount)")
}
if closed {
#if os(Linux)
assertionFailure("Send on closed channel")
#else
NSException.raise(NSExceptionName(rawValue: "Exception"), format: "send on closed channel", arguments: getVaList([]))
#endif
}
msgs.append(msg)
broadcast()
while msgs.count > cap {
cond.wait()
}
}
(我添加了日志记录,并且 threadCount
。一旦发生死锁,threadCount
为 2。我在 Mutex
[=46= 中尝试了相同的 "inc after lock, dec before unlock" ],我在死锁期间得到 3???我不知道怎么回事,我也没有进一步调查,尽管这可能是一个重要的线索。)
如果testCompetingDeadlock
是运行,通常会立即发生死锁,两个发送线程卡在send
的cond.wait()
行,都在send
的锁定区内同一个互斥体。我不知道怎么办。我尝试测试 Mutex
本身,就像我认为 send
使用它一样,如下所示:
private func testSafeMutex() {
let mutex = Mutex()
dispatch {
NSLog("1 locking")
mutex.lock()
NSLog("1 locked")
defer {
NSLog("1 unlocking")
mutex.unlock()
NSLog("1 unlocked")
}
sleep(1)
}
dispatch {
NSLog("2 locking")
mutex.lock()
NSLog("2 locked")
defer {
NSLog("2 unlocking")
mutex.unlock()
NSLog("2 unlocked")
}
sleep(1)
}
}
但是,工作正常 - 没有死锁。
我真的不确定该怎么做,除了添加越来越细粒度的日志记录,并尝试合并两个测试用例直到找到关键差异(这很困难,因为很难保持版本之间的代码功能)。有人可以帮我调试这个库吗?是否存在一些 iOS 特定的内存模型问题等?
我最终弄明白了 - 我忘记了最后的 cond.wait()
释放了锁,这解释了为什么我在一个假定锁定的部分中有多个线程。弄清楚这一点后,我找出了真正的问题并提出了一个解决方案:https://github.com/Erhannis/Safe/commit/cfa41231d01895457bfc1421d779a29a18923c5b
据我所知,真正的问题基本上是退出 while
循环的条件是错误的——所有发送线程都会等到缓冲区有空间,但那些消息已被读取的线程一旦他们的消息被阅读,就应该退出循环。
我正在使用 https://github.com/tidwall/Safe ,一个 Swift 并发库,我想我发现了一个线程错误。 (我正在使用 IOS 12.3.1,iPhone Xs。Swift 4,我想;Xcode 10.2。)图书馆现在是只读的,所以我我正在尝试自己调试它。不过,这个错误真的很微妙,或者它是由我什至没有想到的东西引起的,因为我做了几乎与给定库相同的事情并且它工作正常,但是库本身死锁了。
下面是本不该死锁的测试代码:
private func testCompetingDeadlock() {
NSLog("start")
let c = Chan<Int32>()
let b = Chan<Int32>()
let COUNT = 1000
let wg = WaitGroup()
wg.add(1)
dispatch {
NSLog("receiver starting")
for i in 0..<(2*COUNT) {
Thread.sleep(forTimeInterval: 0.01)
let v = <-c
b <- v!
}
wg.done()
}
sleep(1)
wg.add(1)
dispatch {
NSLog("sender 1 starting")
for i in 0..<COUNT {
c <- 1
<-b
NSLog("1 : \(i)")
}
NSLog("1 done")
wg.done()
}
wg.add(1)
dispatch {
NSLog("sender 2 starting")
for i in 0..<COUNT {
c <- 2
<-b
NSLog("2 : \(i)")
}
NSLog("2 done")
wg.done()
}
wg.wait()
NSLog("Both done")
}
请注意 send
又名 <-
的底层实现是
internal func send(_ msg: T) {
NSLog("locking (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
cond.mutex.lock()
NSLog("locked (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
threadCount += 1
defer {
threadCount -= 1
NSLog("unlocking (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
cond.mutex.unlock()
NSLog("unlocked (\(Thread.current)) - \(Unmanaged.passUnretained(cond.mutex).toOpaque())")
}
if threadCount > 1 {
NSLog("threadCount is \(threadCount)")
}
if closed {
#if os(Linux)
assertionFailure("Send on closed channel")
#else
NSException.raise(NSExceptionName(rawValue: "Exception"), format: "send on closed channel", arguments: getVaList([]))
#endif
}
msgs.append(msg)
broadcast()
while msgs.count > cap {
cond.wait()
}
}
(我添加了日志记录,并且 threadCount
。一旦发生死锁,threadCount
为 2。我在 Mutex
[=46= 中尝试了相同的 "inc after lock, dec before unlock" ],我在死锁期间得到 3???我不知道怎么回事,我也没有进一步调查,尽管这可能是一个重要的线索。)
如果testCompetingDeadlock
是运行,通常会立即发生死锁,两个发送线程卡在send
的cond.wait()
行,都在send
的锁定区内同一个互斥体。我不知道怎么办。我尝试测试 Mutex
本身,就像我认为 send
使用它一样,如下所示:
private func testSafeMutex() {
let mutex = Mutex()
dispatch {
NSLog("1 locking")
mutex.lock()
NSLog("1 locked")
defer {
NSLog("1 unlocking")
mutex.unlock()
NSLog("1 unlocked")
}
sleep(1)
}
dispatch {
NSLog("2 locking")
mutex.lock()
NSLog("2 locked")
defer {
NSLog("2 unlocking")
mutex.unlock()
NSLog("2 unlocked")
}
sleep(1)
}
}
但是,工作正常 - 没有死锁。
我真的不确定该怎么做,除了添加越来越细粒度的日志记录,并尝试合并两个测试用例直到找到关键差异(这很困难,因为很难保持版本之间的代码功能)。有人可以帮我调试这个库吗?是否存在一些 iOS 特定的内存模型问题等?
我最终弄明白了 - 我忘记了最后的 cond.wait()
释放了锁,这解释了为什么我在一个假定锁定的部分中有多个线程。弄清楚这一点后,我找出了真正的问题并提出了一个解决方案:https://github.com/Erhannis/Safe/commit/cfa41231d01895457bfc1421d779a29a18923c5b
据我所知,真正的问题基本上是退出 while
循环的条件是错误的——所有发送线程都会等到缓冲区有空间,但那些消息已被读取的线程一旦他们的消息被阅读,就应该退出循环。