Skip to content

Instantly share code, notes, and snippets.

@ollieatkinson
Created December 26, 2020 18:00
Show Gist options
  • Save ollieatkinson/6e3d9f0547819b7a737cd7d9fd504c2a to your computer and use it in GitHub Desktop.
Save ollieatkinson/6e3d9f0547819b7a737cd7d9fd504c2a to your computer and use it in GitHub Desktop.
Atomic read/write access for collections
import Foundation
class AtomicCollection<C> where C: Collection {
var base: C
let queue: DispatchQueue
init(_ base: C, label: String, qos: DispatchQoS = .unspecified, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil) {
self.base = base
self.queue = DispatchQueue(
label: label,
qos: qos,
attributes: .concurrent,
autoreleaseFrequency: autoreleaseFrequency,
target: target
)
}
}
extension AtomicCollection: Collection {
typealias Index = C.Index
typealias Element = C.Element
typealias SubSequence = C.SubSequence
var startIndex: Index {
queue.sync { base.startIndex }
}
var endIndex: Index {
queue.sync { base.endIndex }
}
var count: Int {
queue.sync { base.count }
}
subscript(position: Index) -> Element {
queue.sync { base[position] }
}
subscript(bounds: Range<Index>) -> SubSequence {
queue.sync { base[bounds] }
}
func index(after i: Index) -> Index {
queue.sync { base.index(after: i) }
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
queue.sync { base.index(i, offsetBy: distance) }
}
func index(_ i: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? {
queue.sync { base.index(i, offsetBy: distance, limitedBy: limit) }
}
@inlinable
public func distance(from start: C.Index, to end: C.Index) -> Int {
queue.sync { base.distance(from: start, to: end) }
}
}
extension AtomicCollection: BidirectionalCollection where C: BidirectionalCollection {
func index(before i: Index) -> Index {
queue.sync { base.index(before: i) }
}
}
extension AtomicCollection: RandomAccessCollection where C: RandomAccessCollection {}
extension AtomicCollection: LazySequenceProtocol where C: LazySequenceProtocol {}
extension AtomicCollection: LazyCollectionProtocol where C: LazyCollectionProtocol {}
extension AtomicCollection: MutableCollection where C: MutableCollection {
subscript(position: Index) -> Element {
get {
queue.sync { base[position] }
}
set {
queue.async(flags: .barrier) { [self] in base[position] = newValue }
}
}
subscript(bounds: Range<Index>) -> SubSequence {
get {
queue.sync { base[bounds] }
}
set {
queue.async(flags: .barrier) { [self] in base[bounds] = newValue }
}
}
}
extension AtomicCollection: Equatable where C.Element: Equatable {
static func == (lhs: AtomicCollection, rhs: AtomicCollection) -> Bool {
lhs.elementsEqual(rhs, by: ==)
}
}
extension AtomicCollection: Hashable where C.Element: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(queue)
for element in self {
hasher.combine(element)
}
}
}
extension AtomicCollection: AnyDictionary where C: AnyDictionary {
typealias Key = C.Key
typealias Value = C.Value
var keys: C.Keys {
queue.sync { base.keys }
}
var values: C.Values {
queue.sync { base.values }
}
subscript(key: Key) -> Value? {
get {
return queue.sync { base[key] }
}
set {
queue.async(flags: .barrier) { [self] in base[key] = newValue }
}
}
subscript(key: Key, default defaultValue: @escaping @autoclosure () -> Value) -> Value {
get {
return queue.sync { base[key, default: defaultValue()] }
}
set {
queue.async(flags: .barrier) { [self] in base[key, default: defaultValue()] = newValue }
}
}
func index(forKey: Key) -> Index? {
queue.sync { base.index(forKey: forKey) }
}
func mapValues<T>(_ transform: (Value) throws -> T) rethrows -> [Key : T] {
try queue.sync { try base.mapValues(transform) }
}
func compactMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> [Key : T] {
try queue.sync { try base.compactMapValues(transform) }
}
func updateValue(_ value: Value, forKey key: Key) -> Value? {
queue.sync { base.updateValue(value, forKey: key) }
}
func merge<S>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows where S: Sequence, S.Element == (Key, Value) {
try queue.sync { try base.merge(other, uniquingKeysWith: combine) }
}
func merge(_ other: [Key: Value], uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows {
try queue.sync { try base.merge(other, uniquingKeysWith: combine) }
}
func merging<S>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows -> [Key: Value] where S: Sequence, S.Element == (Key, Value) {
try queue.sync { try base.merging(other, uniquingKeysWith: combine) }
}
func merging(_ other: [Key: Value], uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows -> [Key: Value] {
try queue.sync { try base.merging(other, uniquingKeysWith: combine) }
}
func remove(at index: Index) -> Element {
queue.sync { base.remove(at: index) }
}
func removeValue(forKey key: Key) -> Value? {
queue.sync { base.removeValue(forKey: key) }
}
func removeAll(keepingCapacity keepCapacity: Bool) {
queue.async(flags: .barrier) { [self] in base.removeAll(keepingCapacity: keepCapacity) }
}
}
extension Collection {
func atomic(
_ label: String = "co.uk.thousandyears.atomic-collection.\(UUID().uuidString)",
qos: DispatchQoS = .unspecified,
autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit,
target: DispatchQueue? = nil
) -> AtomicCollection<Self> {
.init(self, label: label, qos: qos, autoreleaseFrequency: autoreleaseFrequency, target: target)
}
}
@ollieatkinson
Copy link
Author

@ollieatkinson
Copy link
Author

ollieatkinson commented Dec 26, 2020

let dictionary = [String: Int]().atomic()

let group = DispatchGroup()

for index in 0..<1000 {
    group.enter()
    DispatchQueue.global(qos: .utility).async {
        dictionary["item-\(index)"] = index
        group.leave()
    }
}

for index in (0..<1000).reversed() {
    group.enter()
    DispatchQueue.global(qos: .utility).async {
        dictionary["item-\(index)"] = index
        group.leave()
    }
}

for index in 0..<1000 {
    group.enter()
    DispatchQueue.global().async {
        dictionary["item-\(index)"] = index
        group.leave()
    }
}

for index in (0..<1000).reversed() {
    group.enter()
    DispatchQueue.global().async {
        dictionary["item-\(index)"] = index
        group.leave()
    }
}


group.notify(queue: .main) {
    for (key, value) in dictionary {
        assert(key == "item-\(value)")
    }
    print("")
    exit(0)
}

dispatchMain()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment