读者作家并发Swift僵局

时间:2017-06-25 21:05:51

标签: swift multithreading asynchronous concurrency deadlock

我正在尝试修复我的应用程序中的死锁错误。应用程序有一个数组,其中可能有多个线程请求数据。该数组充当缓存,如果数据存在则返回它们。如果没有,则数据不在那里计算,保存到数组然后返回。

背景资讯

我的库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

不幸的是我得到了相同的结果。大部分的运行都取得了成功,但其中一些失败了。所有失败都是因为死锁。

0 个答案:

没有答案