tableView更新期间对数据源的线程安全访问

时间:2018-08-14 09:10:11

标签: ios uitableview thread-safety grand-central-dispatch nscondition

我的应用将constructor( ..., @Optional() @Self() public ngControl: NgControl, ..., ) { // Setting the value accessor directly (instead of using // the providers) to avoid running into a circular import. if (this.ngControl != null) { this.ngControl.valueAccessor = this; } } 与数据源一起使用,该数据源可以由多个线程异步更新。
更改数据源后,将使用

更新tableView,而不是重新加载
tableView

其中tableView.performBatchUpdates({ tableView.deleteRows(at: deletions, with: .automatic) tableView.insertRows(at: insertions, with: .automatic) for (from, to) in movements { tableView.moveRow(at: from, to: to) } }, completion: { (finished) in if !finished { self.tableView.reloadData() } else { // some cleanup code } completion(finished) } 是一些完成代码块。

通常,completion(finished)更新需要0.25秒。在这段时间内,不得更改数据源。确保这一点的最佳方法是什么?

我可以想象我在tableView之前获得了NSCondition,并在完成块中释放了它,当然,对于数据源的所有其他读写访问也是如此,如建议{{3 }}。但是SO上的大多数帖子建议不要使用这种低级同步机制,而应使用GCD。

here将使用线程安全的tableView.performBatchUpdates,该线程安全的并发队列具有同步读取和异步屏障写入,从而允许并发读取。但是我不知道在使用GCD的时候SynchronizedArray期间如何锁定写操作。

是否有解决此问题的标准方法?

1 个答案:

答案 0 :(得分:0)

编辑:

我对下面提供的解决方案不满意,因为它不允许同时读取。
因此,我想出了以下更好的解决方案WriteLockableSynchronizedArray

它基于Basem Emara's SynchronizedArray(再次感谢Basem),即它确实允许并发读取,并且具有以下功能:

  • 它具有一个方便的初始化程序,可以使 WriteLockableSynchronizedArray中的Array,以及返回基础数组的只读属性array
  • 它具有2个函数lockArray()unlockArray(),可以在批处理tableView操作之前和之后调用。
  • 它采用Sequence协议,因此可以使用WriteLockableSynchronizedArray,例如for element in writeLockableSynchronizedArray {}之类的语句中。
  • 如果WriteLockableSynchronizedArray被写锁定,则所有写操作都将延迟直到再次被解锁。解锁后,所有延迟 写入按顺序执行。

这是新的解决方案(推荐):

import Foundation

/// A lockable, thread-safe array.
// It is a modification of Basem Emara's SynchronizedArray, see <http://basememara.com/creating-thread-safe-arrays-in-swift/>
// It provides concurrent reads and serialized writes. A write is only executed after all reads have been completed.
// If the LockableSynchronizedArray is locked, new writes are deferred until it is unlocked again, while new reads are executed normally.
public class WriteLockableSynchronizedArray<Element> {

    typealias WriteOperation = ()->Void

    fileprivate var lockCounter = 0
    fileprivate let queue = DispatchQueue(label: "com.zeh4soft.WriteLockableSynchronizedArray", attributes: .concurrent)
    fileprivate var internalArray = [Element]()
    fileprivate var deferredWriteOperations: [WriteOperation] = []

    /// The internal array of the elements
    var array: [Element]? {
        var result: [Element]?
        queue.sync { result = self.internalArray }
        return result
    }
}

// MARK: - Properties
public extension WriteLockableSynchronizedArray {

    /// The first element of the collection.
    var first: Element? {
        var result: Element?
        queue.sync { result = self.internalArray.first }
        return result
    }

    /// The last element of the collection.
    var last: Element? {
        var result: Element?
        queue.sync { result = self.internalArray.last }
        return result
    }

    /// The number of elements in the array.
    var count: Int {
        var result = 0
        queue.sync { result = self.internalArray.count }
        return result
    }

    /// A Boolean value indicating whether the collection is empty.
    var isEmpty: Bool {
        var result = false
        queue.sync { result = self.internalArray.isEmpty }
        return result
    }

    /// A textual representation of the array and its elements.
    var description: String {
        var result = ""
        queue.sync { result = self.internalArray.description }
        return result
    }
}

// MARK: - Init
public extension WriteLockableSynchronizedArray {
    convenience init(with array: [Element]) {
        self.init()
        self.internalArray = array
    }
}

// MARK: - Lock - Unlock
public extension WriteLockableSynchronizedArray {
    /// Locks the array for writes. Must be unlocked by unlockArray()
    func lockArray() {
        queue.async(flags: .barrier) {
            self.lockCounter += 1
        }
    }

    /// Unlocks the array after it has been locked by lockArray()
    func unlockArray() {
        queue.sync(flags: .barrier) {
            if self.lockCounter > 0 { 
                self.lockCounter -= 1 
            }
            if self.lockCounter == 0 {
                while self.deferredWriteOperations.count > 0 {
                    let nextOp = self.deferredWriteOperations.remove(at: 0)
                    self.queue.async(flags: .barrier) { nextOp() }
                    print("Enqueued deferred write op")
                }
            }
        }
    }
}

// MARK: - Immutable
public extension WriteLockableSynchronizedArray {
    /// Returns the first element of the sequence that satisfies the given predicate or nil if no such element is found.
    ///
    /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
    /// - Returns: The first match or nil if there was no match.
    func first(where predicate: (Element) -> Bool) -> Element? {
        var result: Element?
        queue.sync { result = self.internalArray.first(where: predicate) }
        return result
    }

    /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate.
    ///
    /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array.
    /// - Returns: An array of the elements that includeElement allowed.
    func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
        var result = [Element]()
        queue.sync { result = self.internalArray.filter(isIncluded) }
        return result
    }

    /// Returns the first index in which an element of the collection satisfies the given predicate.
    ///
    /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match.
    /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil.
    func index(where predicate: (Element) -> Bool) -> Int? {
        var result: Int?
        queue.sync { result = self.internalArray.index(where: predicate) }
        return result
    }

    /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements.
    ///
    /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false.
    /// - Returns: A sorted array of the collection’s elements.
    func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
        var result = [Element]()
        queue.sync { result = self.internalArray.sorted(by: areInIncreasingOrder) }
        return result
    }

    /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence.
    ///
    /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value.
    /// - Returns: An array of the non-nil results of calling transform with each element of the sequence.
    func flatMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] {
        var result = [ElementOfResult]()
        queue.sync { result = self.internalArray.compactMap(transform) }
        return result
    }

    /// Calls the given closure on each element in the sequence in the same order as a for-in loop.
    ///
    /// - Parameter body: A closure that takes an element of the sequence as a parameter.
    func forEach(_ body: (Element) -> Void) {
        queue.sync { self.internalArray.forEach(body) }
    }

    /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate.
    ///
    /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match.
    /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false.
    func contains(where predicate: (Element) -> Bool) -> Bool {
        var result = false
        queue.sync { result = self.internalArray.contains(where: predicate) }
        return result
    }
}

// MARK: - Mutable
public extension WriteLockableSynchronizedArray {

    /// Adds a new element at the end of the array.
    ///
    /// - Parameter element: The element to append to the array.
    func append( _ element: Element) {
        let op = { self.internalArray.append(element) }
        handleWriteOperation(op)
    }

    /// Adds a new element at the end of the array.
    ///
    /// - Parameter element: The element to append to the array.
    func append( _ elements: [Element]) {
        let op = { self.internalArray += elements }
        handleWriteOperation(op)
    }

    /// Inserts a new element at the specified position.
    ///
    /// - Parameters:
    ///   - element: The new element to insert into the array.
    ///   - index: The position at which to insert the new element.
    func insert( _ element: Element, at index: Int) {
        let op = { self.internalArray.insert(element, at: index) }
        handleWriteOperation(op)
    }

    /// Removes and returns the element at the specified position.
    ///
    /// - Parameters:
    ///   - index: The position of the element to remove.
    ///   - completion: The handler with the removed element.
    func remove(at index: Int, completion: ((Element) -> Void)? = nil) {
        let op = {
            let element = self.internalArray.remove(at: index)
            DispatchQueue.main.async {
                completion?(element)
            }
        }
        handleWriteOperation(op)
    }

    /// Removes and returns the element at the specified position.
    ///
    /// - Parameters:
    ///   - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
    ///   - completion: The handler with the removed element.
    func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) {
        let op = {
            guard let index = self.internalArray.index(where: predicate) else { return }
            let element = self.internalArray.remove(at: index)
            DispatchQueue.main.async {
                completion?(element)
            }
        }
        handleWriteOperation(op)
    }

    /// Removes all elements from the array.
    ///
    /// - Parameter completion: The handler with the removed elements.
    func removeAll(completion: (([Element]) -> Void)? = nil) {
        let op = {
            let elements = self.internalArray
            self.internalArray.removeAll()
            DispatchQueue.main.async {
                completion?(elements)
            }
        }
        handleWriteOperation(op)
    }
}

public extension WriteLockableSynchronizedArray {

    /// Accesses the element at the specified position if it exists.
    ///
    /// - Parameter index: The position of the element to access.
    /// - Returns: optional element if it exists.
    subscript(index: Int) -> Element? {
        get {
            var result: Element?

            queue.sync {
                guard self.internalArray.startIndex..<self.internalArray.endIndex ~= index else { return }
                result = self.internalArray[index]
            }

            return result
        }
        set {
            guard let newValue = newValue else { return }

            let op = { self.internalArray[index] = newValue }
            handleWriteOperation(op)
        }
    }
}


// MARK: - Equatable
public extension WriteLockableSynchronizedArray where Element: Equatable {

    /// Returns a Boolean value indicating whether the sequence contains the given element.
    ///
    /// - Parameter element: The element to find in the sequence.
    /// - Returns: true if the element was found in the sequence; otherwise, false.
    func contains(_ element: Element) -> Bool {
        var result = false
        queue.sync { result = self.internalArray.contains(element) }
        return result
    }
}

// MARK: - Infix operators
public extension WriteLockableSynchronizedArray {

    static func +=(left: inout WriteLockableSynchronizedArray, right: Element) {
        left.append(right)
    }

    static func +=(left: inout WriteLockableSynchronizedArray, right: [Element]) {
        left.append(right)
    }
}

// MARK: - Protocol Sequence
extension WriteLockableSynchronizedArray: Sequence {

    public func makeIterator() -> Iterator {
        return Iterator(self.array)
    }

    public struct Iterator: IteratorProtocol {
        private var index: Int
        private var arr: [Element]?

        init(_ array: [Element]?) {
            self.arr = array
            index = 0
        }

        mutating public func next() -> Element? {
            guard let arr = self.arr, arr.count > index else { return nil }
            let returnValue = arr[index]
            index += 1
            return returnValue
        }
    }
}

// MARK: - Private helper
fileprivate extension WriteLockableSynchronizedArray {
    func handleWriteOperation(_ op: @escaping WriteLockableSynchronizedArray.WriteOperation) {
        queue.sync { 
            if self.lockCounter > 0 {
                self.deferredWriteOperations.append { op() }
            } else {
                queue.async(flags: .barrier) {
                    op()
                }
            }
        }
    }

}

这是我以前的解决方案(不再推荐):

  • 我实现了LockableArray类,它是对Basem Emara的(谢谢!)SynchronizedArray的修改,它使用递归锁来同步对数组的访问。
  • 我实现了一个方便的初始化程序,该初始化程序从LockableArray生成一个Array,并创建一个只读属性,该属性返回基础Array
  • 我定义了两个函数lockArray()unlockArray(),可以在批处理tableView操作之前和之后调用。
  • 我采用了Sequence协议,因此可以使用LockableArray,例如在for element in lockablaArray {}之类的语句中。

此解决方案的一个缺点是不能像Basem Emara的SynchronizedArray中那样同时执行多次读取。

这里是实现:

import Foundation

/// A lockable, thread-safe array.
// It is a modification of Basem Emara's SynchronizedArray, see <http://basememara.com/creating-thread-safe-arrays-in-swift/>
// It does not use dispatch queues, but a recursive lock, so that multiple array operations can be locked as a group.
// Additions to Basem Emara's implementation:
// - A convenience initializer make a LockableArray from an Array, and a readonly property allows to acces the underlying Array.
// - Protocol Sequence is adopted, so that statements like "for .. in .." can be used.

public class LockableArray<Element> {
    fileprivate var lock = NSRecursiveLock() // Must be recursive, so that batch accesses can be locked together
    fileprivate var privateArray = [Element]()

    /// The internal array of the elements
    var array: [Element]? {
        let result: [Element]
        lock.lock()
        result = privateArray
        lock.unlock()
        return result
    }
}

// MARK: - Properties
public extension LockableArray {

    /// The first element of the collection.
    var first: Element? {
        var result: Element?
        lock.lock()
        result = self.privateArray.first
        lock.unlock()
        return result
    }

    /// The last element of the collection.
    var last: Element? {
        var result: Element?
        lock.lock()
        result = self.privateArray.last
        lock.unlock()
        return result
    }

    /// The number of elements in the array.
    var count: Int {
        var result = 0
        lock.lock()
        result = self.privateArray.count
        lock.unlock()
        return result
    }

    /// A Boolean value indicating whether the collection is empty.
    var isEmpty: Bool {
        var result = false
        lock.lock()
        result = self.privateArray.isEmpty
        lock.unlock()
        return result
    }

    /// A textual representation of the array and its elements.
    var description: String {
        var result = ""
        lock.lock()
        result = self.privateArray.description
        lock.unlock()
        return result
    }
}

// MARK: - Init
public extension LockableArray {
    convenience init(with array: [Element]) {
        self.init()
        self.privateArray = array
    }
}

// MARK: - Lock - Unlock
public extension LockableArray {
    /// Locks the array for multiple writes. Must be unlocked by unlockArray()
    func lockArray() {
        lock.lock()
    }

    /// Unlocks the array after it has been locked by lockArray()
    func unlockArray() {
        lock.unlock()
    }
}

// MARK: - Immutable
public extension LockableArray {

    /// Returns the first element of the sequence that satisfies the given predicate or nil if no such element is found.
    ///
    /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
    /// - Returns: The first match or nil if there was no match.
    func first(where predicate: (Element) -> Bool) -> Element? {
        var result: Element?
        lock.lock()
        result = self.privateArray.first(where: predicate)
        lock.unlock()
        return result
    }

    /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate.
    ///
    /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array.
    /// - Returns: An array of the elements that includeElement allowed.
    func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
        var result = [Element]()
        lock.lock()
        result = self.privateArray.filter(isIncluded)
        lock.unlock()
        return result
    }

    /// Returns the first index in which an element of the collection satisfies the given predicate.
    ///
    /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match.
    /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil.
    func index(where predicate: (Element) -> Bool) -> Int? {
        var result: Int?
        lock.lock()
        result = self.privateArray.index(where: predicate)
        lock.unlock()
        return result
    }

    /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements.
    ///
    /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false.
    /// - Returns: A sorted array of the collection’s elements.
    func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
        var result = [Element]()
        lock.lock()
        result = self.privateArray.sorted(by: areInIncreasingOrder)
        lock.unlock()
        return result
    }

    /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence.
    ///
    /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value.
    /// - Returns: An array of the non-nil results of calling transform with each element of the sequence.
    func compactMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] {
        var result = [ElementOfResult]()
        lock.lock()
        result = self.privateArray.compactMap(transform)
        lock.unlock()
        return result
    }

    /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence.
    ///
    /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value.
    /// - Returns: An array of the non-nil results of calling transform with each element of the sequence.
    func map<ElementOfResult>(_ transform: (Element) -> ElementOfResult) -> [ElementOfResult] {
        var result = [ElementOfResult]()
        lock.lock()
        result = self.privateArray.map(transform)
        lock.unlock()
        return result
    }

    /// Calls the given closure on each element in the sequence in the same order as a for-in loop.
    ///
    /// - Parameter body: A closure that takes an element of the sequence as a parameter.
    func forEach(_ body: (Element) -> Void) {
        lock.lock()
        self.privateArray.forEach(body)
        lock.unlock()
    }

    /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate.
    ///
    /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match.
    /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false.
    func contains(where predicate: (Element) -> Bool) -> Bool {
        var result = false
        lock.lock()
        result = self.privateArray.contains(where: predicate)
        lock.unlock()
        return result
    }
}

// MARK: - Mutable
public extension LockableArray {

    /// Adds a new element at the end of the array.
    ///
    /// - Parameter element: The element to append to the array.
    func append( _ element: Element) {
        lock.lock()
        self.privateArray.append(element)
        lock.unlock()
    }

    /// Adds a new element at the end of the array.
    ///
    /// - Parameter element: The element to append to the array.
    func append( _ elements: [Element]) {
        lock.lock()
        self.privateArray += elements
        lock.unlock()
    }

    /// Inserts a new element at the specified position.
    ///
    /// - Parameters:
    ///   - element: The new element to insert into the array.
    ///   - index: The position at which to insert the new element.
    func insert( _ element: Element, at index: Int) {
        lock.lock()
        self.privateArray.insert(element, at: index)
        lock.unlock()
    }

    /// Removes and returns the element at the specified position.
    ///
    /// - Parameters:
    ///   - index: The position of the element to remove.
    ///   - completion: The handler with the removed element.
    func remove(at index: Int, completion: ((Element) -> Void)? = nil) {
        lock.lock()
        let element = self.privateArray.remove(at: index)
        DispatchQueue.main.async {
            completion?(element)
        }
        lock.unlock()
    }

    /// Removes and returns the element at the specified position.
    ///
    /// - Parameters:
    ///   - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
    ///   - completion: The handler with the removed element.
    func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) {
        lock.lock()
        guard let index = self.privateArray.index(where: predicate) else { return }
        let element = self.privateArray.remove(at: index)
        DispatchQueue.main.async {
            completion?(element)
        }
        lock.unlock()
    }

    /// Removes all elements from the array.
    ///
    /// - Parameter completion: The handler with the removed elements.
    func removeAll(completion: (([Element]) -> Void)? = nil) {
        lock.lock()
        let elements = self.privateArray
        self.privateArray.removeAll()
        DispatchQueue.main.async {
            completion?(elements)
        }
        lock.unlock()
    }
}

public extension LockableArray {

    /// Accesses the element at the specified position if it exists.
    ///
    /// - Parameter index: The position of the element to access.
    /// - Returns: optional element if it exists.
    subscript(index: Int) -> Element? {
        get {
            var result: Element?
            lock.lock()
            guard self.privateArray.startIndex ..< self.privateArray.endIndex ~= index else { return nil }
            result = self.privateArray[index]
            lock.unlock()
            return result
        }
        set {
            guard let newValue = newValue else { return }
            lock.lock()
            self.privateArray[index] = newValue
            lock.unlock()
        }
    }
}

// MARK: - Equatable
public extension LockableArray where Element: Equatable {

    /// Returns a Boolean value indicating whether the sequence contains the given element.
    ///
    /// - Parameter element: The element to find in the sequence.
    /// - Returns: true if the element was found in the sequence; otherwise, false.
    func contains(_ element: Element) -> Bool {
        var result = false
        lock.lock()
        result = self.privateArray.contains(element)
        lock.unlock()
        return result
    }
}

// MARK: - Infix operators
public extension LockableArray {

    static func +=(left: inout LockableArray, right: Element) {
        left.append(right)
    }

    static func +=(left: inout LockableArray, right: [Element]) {
        left.append(right)
    }
}

// MARK: - Protocol Sequence
extension LockableArray: Sequence {

    public func makeIterator() -> Iterator {
        return Iterator(self.array)
    }

    public struct Iterator: IteratorProtocol {
        private var index: Int
        private var arr: [Element]?

        init(_ array: [Element]?) {
            self.arr = array
            index = 0
        }

        mutating public func next() -> Element? {
            guard let arr = self.arr, arr.count > index else { return nil }
            let returnValue = arr[index]
            index += 1
            return returnValue
        }
    }
}