Thread-safe access to a datasource during a tableView update
Thread-safe access to a datasource during a tableView update
My app uses a tableView
with a data source that can be updated asynchronously by multiple threads.
When the data source is changed, the tableView
is updated, not reloaded, using
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)
where completion(finished)
is some completion block.
completion(finished)
Typically, the tableView
update takes 0.25 sec. During this time, the data source must not be changed. What is the best method to ensure this?
tableView
I can imagine that I acquire an NSCondition
before tableView.performBatchUpdates
, and release it in the completion block, of course also for every other read and write access to the data source, as suggested here. But most posts on SO suggest not to use such low-level sync mechanism, but use GCD instead.
NSCondition
tableView.performBatchUpdates
One suggestion is to use a thread-safe SynchronizedArray
using a concurrent queue with sync reads and async barrier writes, which allows concurrent reads. But I don’t know how I could lock out writes during tableView.performBatchUpdates
when I use GCD.
SynchronizedArray
tableView.performBatchUpdates
Is there a standard solution to this problem?
1 Answer
1
EDIT:
I was not happy with the solution I provided below, since it does not allow concurrent reads.
Thus I came up with the following better solution, a WriteLockableSynchronizedArray
.
WriteLockableSynchronizedArray
It is based much closer on Basem Emara's SynchronizedArray (thanks again, Basem), i.e. it does allow concurrent reads, and has the following features:
WriteLockableSynchronizedArray
Array
array
lockArray()
unlockArray()
Sequence
WriteLockableSynchronizedArray
for element in writeLockableSynchronizedArray
WriteLockableSynchronizedArray
Here is the new solution (recommended):
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()
Here is my previous solution (no longer recommended):
LockableArray
SynchronizedArray
LockableArray
Array
Array
lockArray()
unlockArray()
Sequence
LockableArray
for element in lockablaArray
A disadvantage of this solution is that multiple reads can not be executed concurrently as in Basem Emara's SynchronizedArray
.
SynchronizedArray
Here is the implementation:
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
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.