diff --git a/README.md b/README.md index dabf6b4..50a8796 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ if index < concurrentArray.count { ``` ## Priority queue -**SCC** provides classical (non-concurrent) priority queue +**SCC** provides both classical and concurrent priority queues ```swift var priorityQueue = PriorityQueue(<) @@ -64,3 +64,4 @@ while priorityQueue.count > 0 { As you can see `PriorityQueue(<)` constructs min-queue, with `PriorityQueue(>)` you can get max-queue. If you need to reserve capacity right away, use `PriorityQueue(capacity: 1024, comparator: <)`. +`ConcurrentPriorityQueue(<)` creates a thread-safe version, with a very similar interface. diff --git a/SwiftConcurrentCollections.xcodeproj/project.pbxproj b/SwiftConcurrentCollections.xcodeproj/project.pbxproj index 9f03b4d..0ad9700 100644 --- a/SwiftConcurrentCollections.xcodeproj/project.pbxproj +++ b/SwiftConcurrentCollections.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 2F897F0823E8692800B522AF /* ConcurrentDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F897F0723E8692800B522AF /* ConcurrentDictionary.swift */; }; 2F897F0C23E86A8900B522AF /* ConcurrentArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F897F0B23E86A8900B522AF /* ConcurrentArray.swift */; }; 2F897F0E23E8757400B522AF /* ConcurrentArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F897F0D23E8757400B522AF /* ConcurrentArrayTests.swift */; }; + D2ED2DF125F375C100BA4B77 /* ConcurrentPriorityQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED2DF025F375C100BA4B77 /* ConcurrentPriorityQueue.swift */; }; + D2ED2DF525F3789400BA4B77 /* ConcurrentPriorityQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED2DF425F3789400BA4B77 /* ConcurrentPriorityQueueTests.swift */; }; D2FD7F7925713C750014F8E8 /* PriorityQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD7F7825713C750014F8E8 /* PriorityQueue.swift */; }; D2FD7F7D257147470014F8E8 /* PriorityQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD7F7C257147470014F8E8 /* PriorityQueueTests.swift */; }; /* End PBXBuildFile section */ @@ -44,6 +46,8 @@ 2F897F0B23E86A8900B522AF /* ConcurrentArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentArray.swift; sourceTree = ""; }; 2F897F0D23E8757400B522AF /* ConcurrentArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentArrayTests.swift; sourceTree = ""; }; 2F897F0F23E87A1800B522AF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + D2ED2DF025F375C100BA4B77 /* ConcurrentPriorityQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentPriorityQueue.swift; sourceTree = ""; }; + D2ED2DF425F3789400BA4B77 /* ConcurrentPriorityQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConcurrentPriorityQueueTests.swift; sourceTree = ""; }; D2FD7F7825713C750014F8E8 /* PriorityQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriorityQueue.swift; sourceTree = ""; }; D2FD7F7C257147470014F8E8 /* PriorityQueueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriorityQueueTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -97,6 +101,7 @@ 2F897EF123E8683000B522AF /* Info.plist */, 2F7CD5F623F051C00060F6BD /* RWLock.swift */, D2FD7F7825713C750014F8E8 /* PriorityQueue.swift */, + D2ED2DF025F375C100BA4B77 /* ConcurrentPriorityQueue.swift */, ); path = SwiftConcurrentCollections; sourceTree = ""; @@ -106,6 +111,7 @@ children = ( 2F897EFB23E8683000B522AF /* ConcurrentDictionaryTests.swift */, 2F897F0D23E8757400B522AF /* ConcurrentArrayTests.swift */, + D2ED2DF425F3789400BA4B77 /* ConcurrentPriorityQueueTests.swift */, D2FD7F7C257147470014F8E8 /* PriorityQueueTests.swift */, 2F897EFD23E8683000B522AF /* Info.plist */, ); @@ -227,6 +233,7 @@ 2F897F0823E8692800B522AF /* ConcurrentDictionary.swift in Sources */, 2F7CD5F923F055610060F6BD /* GCDConcurrentDictionary.swift in Sources */, D2FD7F7925713C750014F8E8 /* PriorityQueue.swift in Sources */, + D2ED2DF125F375C100BA4B77 /* ConcurrentPriorityQueue.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -236,6 +243,7 @@ files = ( 2F897F0E23E8757400B522AF /* ConcurrentArrayTests.swift in Sources */, 2F897EFC23E8683000B522AF /* ConcurrentDictionaryTests.swift in Sources */, + D2ED2DF525F3789400BA4B77 /* ConcurrentPriorityQueueTests.swift in Sources */, D2FD7F7D257147470014F8E8 /* PriorityQueueTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftConcurrentCollections/ConcurrentPriorityQueue.swift b/SwiftConcurrentCollections/ConcurrentPriorityQueue.swift new file mode 100644 index 0000000..eafe189 --- /dev/null +++ b/SwiftConcurrentCollections/ConcurrentPriorityQueue.swift @@ -0,0 +1,82 @@ +import Foundation + +/// Thread-safe priority queue wrapper +/// - Important: Note that this is a `class`, i.e. reference (not value) type +public final class ConcurrentPriorityQueue { + + private var container: PriorityQueue + private let rwlock = RWLock() + + public var count: Int { + rwlock.readLock() + defer { + rwlock.unlock() + } + return container.count + } + + public init(capacity: Int, complexComparator: @escaping PriorityQueue.ComplexComparator) { + self.container = PriorityQueue(capacity: capacity, complexComparator: complexComparator) + } + + public init(capacity: Int, comparator: @escaping PriorityQueue.Comparator) { + self.container = PriorityQueue(capacity: capacity, comparator: comparator) + } + + /// Creates `ConcurrentPriorityQueue` with default capacity + /// - Parameter comparator: heap property will hold if `comparator(parent(i), i)` is `true` + /// e.g. `<` will create a minimum-queue and `>` - maximum-queue + public init(_ comparator: @escaping PriorityQueue.Comparator) { + self.container = PriorityQueue(comparator) + } + + public func insert(_ value: Element) { + rwlock.writeLock() + container.insert(value) + rwlock.unlock() + } + + /// Remove top element from the queue + public func pop() -> Element { + rwlock.writeLock() + defer { + rwlock.unlock() + } + + return container.pop() + } + + /// Get top element from the queue + public func peek() -> Element { + rwlock.readLock() + defer { + rwlock.unlock() + } + + return container.peek() + } + + /// Get top element from the queue, returns `nil`if queue is empty + public func safePeek() -> Element? { + rwlock.readLock() + defer { + rwlock.unlock() + } + return container.count > 0 + ? container.peek() + : nil + } + + /// Remove top element from the queue, returns `nil`if queue is empty + public func safePop() -> Element? { + rwlock.writeLock() + defer { + rwlock.unlock() + } + + return container.count > 0 + ? container.pop() + : nil + } + +} diff --git a/SwiftConcurrentCollections/PriorityQueue.swift b/SwiftConcurrentCollections/PriorityQueue.swift index d6a2197..8f5b591 100644 --- a/SwiftConcurrentCollections/PriorityQueue.swift +++ b/SwiftConcurrentCollections/PriorityQueue.swift @@ -10,11 +10,11 @@ import Foundation private let defaultCapacity = 16 -public struct PriorityQueue { - public typealias ComplexComparator = (V, V) -> ComparisonResult - public typealias Comparator = (V, V) -> Bool +public struct PriorityQueue { + public typealias ComplexComparator = (Element, Element) -> ComparisonResult + public typealias Comparator = (Element, Element) -> Bool - private var container: [V] + private var container: [Element] private var heapSize = 0 private let comparator: Comparator @@ -34,7 +34,7 @@ public struct PriorityQueue { public init(capacity: Int, comparator: @escaping Comparator) { self.comparator = comparator - container = Array() + container = Array() container.reserveCapacity(capacity) } @@ -45,7 +45,7 @@ public struct PriorityQueue { self.init(capacity: defaultCapacity, comparator: comparator) } - mutating public func insert(_ value: V) { + mutating public func insert(_ value: Element) { if heapSize == container.capacity { let reserveSize = container.count > 0 ? container.count : defaultCapacity container.reserveCapacity(container.count + reserveSize) @@ -64,7 +64,7 @@ public struct PriorityQueue { } /// Remove top element from the queue - mutating public func pop() -> V { + mutating public func pop() -> Element { if heapSize <= 0 { fatalError("Fatal error: trying to pop from an empty PriorityQueue") } @@ -83,7 +83,7 @@ public struct PriorityQueue { } /// Get top element from the queue - public func peek() -> V { + public func peek() -> Element { if heapSize <= 0 { fatalError("Fatal error: trying to peek into an empty PriorityQueue") } diff --git a/SwiftConcurrentCollectionsTests/ConcurrentPriorityQueueTests.swift b/SwiftConcurrentCollectionsTests/ConcurrentPriorityQueueTests.swift new file mode 100644 index 0000000..bdf7f0c --- /dev/null +++ b/SwiftConcurrentCollectionsTests/ConcurrentPriorityQueueTests.swift @@ -0,0 +1,78 @@ +import XCTest +@testable import SwiftConcurrentCollections + +class ConcurrentPriorityQueueTests: XCTestCase { + + func testConcurrentReadingAndWriting() { + let startDate = Date().timeIntervalSince1970 + let concurrentPriorityQueue = ConcurrentPriorityQueue(<) + // Unsafe version: + // var concurrentPriorityQueue = PriorityQueue(<) + + let readingQueue = DispatchQueue( + label: "ConcurrentArrayTests.readingQueue", + qos: .userInteractive, + attributes: .concurrent + ) + + let writingQueue = DispatchQueue( + label: "ConcurrentArrayTests.writingQueue", + qos: .userInteractive, + attributes: .concurrent + ) + + // 100 seems to be small enough not to cause performance problems + for i in 0 ..< 100 { + let writeExpectation = expectation(description: "Write expectation") + let readExpectation = expectation(description: "Read expectation") + + writingQueue.async { + concurrentPriorityQueue.insert(i) + writeExpectation.fulfill() + } + + readingQueue.async { + let count = concurrentPriorityQueue.count + guard count > 0 else { + return + } + + let value = concurrentPriorityQueue.peek() + print(value) + readExpectation.fulfill() + } + } + + waitForExpectations(timeout: 10, handler: nil) + print("\(#function) took \(Date().timeIntervalSince1970 - startDate) seconds") + } + + func testPop() { + let concurrentPriorityQueue = ConcurrentPriorityQueue(<) + + concurrentPriorityQueue.insert(1) + XCTAssertEqual(concurrentPriorityQueue.count, 1) + XCTAssertEqual(concurrentPriorityQueue.peek(), 1) + XCTAssertEqual(concurrentPriorityQueue.pop(), 1) + XCTAssertEqual(concurrentPriorityQueue.count, 0) + } + + func testSafePop() { + let concurrentPriorityQueue = ConcurrentPriorityQueue(<) + + XCTAssertEqual(concurrentPriorityQueue.safePop(), nil) + concurrentPriorityQueue.insert(1) + XCTAssertEqual(concurrentPriorityQueue.safePop(), 1) + XCTAssertEqual(concurrentPriorityQueue.count, 0) + } + + func testSafePeek() { + let concurrentPriorityQueue = ConcurrentPriorityQueue(<) + + XCTAssertEqual(concurrentPriorityQueue.safePeek(), nil) + concurrentPriorityQueue.insert(1) + XCTAssertEqual(concurrentPriorityQueue.safePeek(), 1) + XCTAssertEqual(concurrentPriorityQueue.count, 1) + } + +}