在 tableView 更新期间对数据源的线程安全访问
Thread-safe access to a datasource during a tableView update
我的应用程序使用 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)
是一些完成块。
通常,tableView
更新需要 0.25 秒。在此期间,不得更改数据源。确保这一点的最佳方法是什么?
我可以想象,我在tableView.performBatchUpdates
之前获得了一个NSCondition
,并在完成块中释放它,当然也为每个其他对数据源的读写访问,如建议的那样here。但是SO上的大多数帖子都建议不要使用这种低级同步机制,而是使用GCD。
One suggestion是使用线程安全的SynchronizedArray
使用同步读异步屏障写的并发队列,允许并发读。但是我不知道如何在 tableView.performBatchUpdates
使用 GCD 时锁定写入。
这个问题有标准的解决方案吗?
编辑:
我对下面提供的解决方案不满意,因为它不允许并发读取。
因此,我想出了以下更好的解决方案,WriteLockableSynchronizedArray
。
它更接近于 Basem Emara's SynchronizedArray(再次感谢 Basem),即它确实允许并发读取,并具有以下特性:
- 它有一个方便的初始化程序
WriteLockableSynchronizedArray
来自 Array
,以及只读 属性 array
即 returns 底层数组。
- 它有 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()
}
}
}
}
}
这是我之前的解决方案(不再推荐):
- 我实现了 class
LockableArray
,这是对 Basem Emara 的修改(谢谢!)SynchronizedArray
,它使用递归锁来同步对数组的访问。
- 我实现了一个方便的初始化程序,它从
Array
生成 LockableArray
,以及 returns 底层 Array
的只读 属性。
- 我定义了 2 个函数
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
}
}
}
我的应用程序使用 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)
是一些完成块。
通常,tableView
更新需要 0.25 秒。在此期间,不得更改数据源。确保这一点的最佳方法是什么?
我可以想象,我在tableView.performBatchUpdates
之前获得了一个NSCondition
,并在完成块中释放它,当然也为每个其他对数据源的读写访问,如建议的那样here。但是SO上的大多数帖子都建议不要使用这种低级同步机制,而是使用GCD。
One suggestion是使用线程安全的SynchronizedArray
使用同步读异步屏障写的并发队列,允许并发读。但是我不知道如何在 tableView.performBatchUpdates
使用 GCD 时锁定写入。
这个问题有标准的解决方案吗?
编辑:
我对下面提供的解决方案不满意,因为它不允许并发读取。
因此,我想出了以下更好的解决方案,WriteLockableSynchronizedArray
。
它更接近于 Basem Emara's SynchronizedArray(再次感谢 Basem),即它确实允许并发读取,并具有以下特性:
- 它有一个方便的初始化程序
WriteLockableSynchronizedArray
来自Array
,以及只读 属性array
即 returns 底层数组。 - 它有 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()
}
}
}
}
}
这是我之前的解决方案(不再推荐):
- 我实现了 class
LockableArray
,这是对 Basem Emara 的修改(谢谢!)SynchronizedArray
,它使用递归锁来同步对数组的访问。 - 我实现了一个方便的初始化程序,它从
Array
生成LockableArray
,以及 returns 底层Array
的只读 属性。 - 我定义了 2 个函数
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
}
}
}