我正在尝试修复我的应用程序中的死锁错误。应用程序有一个数组,其中可能有多个线程请求数据。该数组充当缓存,如果数据存在则返回它们。如果没有,则数据不在那里计算,保存到数组然后返回。
背景资讯
我的库PazProtector支持同步读取和异步写入。在写作时,它使用屏障来防止其他人阅读。
它的设计方式应该是死锁的万无一失,因为它不提供对阵列的直接访问。我也支持词典和其他各种对象。我对所有这些都得到了相同的结果。
为了追踪问题,我创建了一个带有测试的框架。当使用少量迭代时,测试成功。当我试图推动它时,测试失败了。应用程序也会发生同样的事情。当大量数据被更新时,它将死锁,这意味着多次读写。
读写锁定
为方便起见,我创建了一个读写锁。它的功能非常简单,它有一个队列,它提供读同步访问和写异步块访问。
public class PazReadWriteLock {
public private (set) lazy var queue: DispatchQueue = DispatchQueue(label: self.lockName, attributes: .concurrent)
public init(lockName: String) {
self.lockName = lockName
}
public convenience init(randomLockName: String) {
let lockName = PazReadWriteLock.RandomLockName(randomLockName)
self.init(lockName: lockName)
}
public class func RandomLockName(_ prefix: String) -> String {
let random = Int(arc4random_uniform(10000))
return "\(prefix).\(random)"
}
public private (set) var lockName: String
public func withReadLock(_ closure: @escaping () -> Void) {
self.queue.sync {
closure()
}
}
public func withWriteLock(_ closure: @escaping () -> Void) {
self.queue.async(flags: DispatchWorkItemFlags.barrier) {
closure()
}
}
}
PazProtector
然后我创建了另一个名为PazProtector的类,它使用PazReadWriteLock来保护项目。该类是通用的,因此该项可以是任何类型的对象。
public class PazProtector<T> {
private let lock : PazReadWriteLock
private var item: T
public init(name: String, item: T) {
self.lock = PazReadWriteLock(lockName: name)
self.item = item
}
public convenience init(item: T) {
self.init(name: PazReadWriteLock.RandomLockName("PazProtector"), item: item)
}
public func withReadLock(_ block: @escaping (T) -> Void) {
lock.withReadLock() { [weak self] in
guard let strongSelf = self else {
return
}
block(strongSelf.item)
}
}
public func withWriteLock(_ block: @escaping (inout T) -> Void) {
lock.withWriteLock() { [weak self] in
guard let strongSelf = self else {
return
}
block(&strongSelf.item)
}
}
}
要将PazProtector用于数组,我们初始化:
let array = PazProtector<Array<Int>>()
要读取数组,我们使用读取块
var firstElement: ?
self.withReadLock { (array) in
firstElement = array[0]
}
print(firstElement)
要在数组上写入,我们使用写块:
self.withWriteLock { (array) in
array[0] = 0
}
PazProtectorArray
为了简化我创建的PazProtectedArray:
public class PazProtectedArray<Element>: PazProtector<Array<Element>> {
public init(lockName: String) {
let item = Array<Element>()
super.init(name: lockName, item: item)
}
public subscript(index: Int) -> Element? {
get {
var result: Element?
self.withReadLock { (array) in
result = array[index]
}
return result
}
set(object) {
self.withWriteLock { (array) in
if let letObject = object {
array[index] = letObject
} else {
array.remove(at: index)
}
}
}
}
public func append(_ item: Element) {
self.withWriteLock { (array) in
array.append(item)
}
}
public var count: Int {
var result: Int = 0
self.withReadLock { (array) in
result = array.count
}
return result
}
}
所以现在我们几乎可以将它用作普通数组,而不必担心阻塞。我们唯一需要注意的是写入是异步的,所以当我们不在块时,写操作可能仍然在运行。但由于它是一个障碍块,这应该不是问题,我们尝试同时读取,因为写操作将阻止它直到写完成。
死锁
我希望我对创建此框架的假设是正确的。我已经使用了一段时间,没有任何明显的问题。最近我在一个应用程序上使用它,当新数据进入时,它会执行数千次读写操作。我注意到应用程序会冻结。当我在调试器上运行它时,我跟踪了锁定的读操作的冻结。
测试
所以我决定写一个测试。最初的测试没有突破极限。迭代次数为1000,测试正在通过。然后我决定将它推到2000和5同时异步运行相同的测试。这是我注意到一些失败的时候。为了证明我的情况,我一个接一个地运行整个事情50次。这是我一直失败的时候。
在waitForExpectations上发生故障并且等待5或60秒的时间并不重要,就像发生死锁一样,它永远不会解锁。测试的最终版本如下:
func testPazProtectedArray() {
for run in 0...50 {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
for x in 0...runs {
let tidalExpectation = self.expectation(description: "\(run):protector\(x)")
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
// Thread-safe array
let protectedArray = PazProtectedArray<Int>(randomLockName: "array\(x)")
var i = self.iterations
DispatchQueue.concurrentPerform(iterations: self.iterations) { index in
let last = protectedArray.last ?? 0
protectedArray.append(last + 1)
i -= 1
// Final loop
guard i <= 0 else { return }
//print("a: \(x): \(protectedArray.count) \(protectedArray.last ?? 0)")
XCTAssert(protectedArray.count == self.iterations)
tidalExpectation.fulfill()
}
}
}
waitForExpectations(timeout: 5) { (error) in
if let _ = error {
XCTFail("Timeout")
}
}
}
}
Git存储库
您可以在以下存储库中找到包含测试的完整项目:
https://github.com/zirinisp/PazProtector.git
我试图简化它并使用以下示例中提供的数组:
https://gist.github.com/basememara/afaae5310a6a6b97bdcdbe4c2fdcd0c6
以上示例大部分时间都通过了1000次迭代测试。我尝试了5-6次然后我得到了一个错误。如果我在多次迭代和运行中使用我的测试,它将始终失败。所以它与我拥有PazProtectedArray - &gt;这一事实无关。 PazProtector - &gt; PazReadWriteLock
同样在我的应用程序中,当同时发生多个更新时,会发生死锁。我注意到添加简单的print语句会改变行为,并在以后或我的代码中使用PazProtector的另一个点发生死锁。
更新26/06/2017
今天早上我尝试用以下内容替换我的读写锁:
https://github.com/WeltN24/PiedPiper/blob/master/PiedPiper/ReadWriteLock.swift
不幸的是我得到了相同的结果。大部分的运行都取得了成功,但其中一些失败了。所有失败都是因为死锁。