diff --git a/NGTools.xcodeproj/project.pbxproj b/NGTools.xcodeproj/project.pbxproj index 2494d43..0f53505 100644 --- a/NGTools.xcodeproj/project.pbxproj +++ b/NGTools.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ B295F3B4290772E500CB4087 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B295F3B3290772E500CB4087 /* CryptoSwift */; }; EDA367DA2BDBF7D90070383D /* VersionRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA367D92BDBF7D90070383D /* VersionRange.swift */; }; EDA367DC2BDBF8430070383D /* VersionRangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA367DB2BDBF8430070383D /* VersionRangeTests.swift */; }; + F718AA012C10B4E700137A5E /* ThreadSafeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F718AA002C10B4E700137A5E /* ThreadSafeSet.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -133,6 +134,7 @@ B295F3AD2907729D00CB4087 /* CryptoRSA.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoRSA.swift; sourceTree = ""; }; EDA367D92BDBF7D90070383D /* VersionRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionRange.swift; sourceTree = ""; }; EDA367DB2BDBF8430070383D /* VersionRangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionRangeTests.swift; sourceTree = ""; }; + F718AA002C10B4E700137A5E /* ThreadSafeSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeSet.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -208,6 +210,7 @@ A42157BA26EA34CA008C21D2 /* Utils */ = { isa = PBXGroup; children = ( + F718AA002C10B4E700137A5E /* ThreadSafeSet.swift */, A4F8E8292A86BD1C0058F809 /* Storage */, A4C75F4E27BED95B00F752A3 /* Migration */, A42157BB26EA34CA008C21D2 /* curry.swift */, @@ -460,6 +463,7 @@ A421580E26EA350F008C21D2 /* String+Extensions.swift in Sources */, A421580526EA350F008C21D2 /* URL+Extensions.swift in Sources */, A4C75F5027BED96E00F752A3 /* MigrationRunner.swift in Sources */, + F718AA012C10B4E700137A5E /* ThreadSafeSet.swift in Sources */, A421580C26EA350F008C21D2 /* NSDirectionalEdgeInsets+Extensions.swift in Sources */, A494C0B927C40EC700EA0C37 /* MigrationTaskState.swift in Sources */, B295F3B02907729D00CB4087 /* CryptoRSA.swift in Sources */, diff --git a/Sources/NGTools/Utils/ThreadSafeSet.swift b/Sources/NGTools/Utils/ThreadSafeSet.swift new file mode 100644 index 0000000..bef87f5 --- /dev/null +++ b/Sources/NGTools/Utils/ThreadSafeSet.swift @@ -0,0 +1,44 @@ +// +// File.swift +// +// +// Created by Jeanne Creantor, N'rick (Ordinateur) on 2024-06-04. +// + +import Foundation + +public class ThreadSafeSet { + private var set: Set = [] + private let queue = DispatchQueue(label: "com.nuglif.safeThreadSet.\(T.self)", attributes: .concurrent) + + public init(set: Set = []) { + self.set = set + } + + public func insert(_ newElement: T) { + queue.async(flags: .barrier) { + self.set.insert(newElement) + } + } + + public func remove(_ member: T) { + queue.async(flags: .barrier) { + self.set.remove(member) + } + } + + public func removeAll() { + queue.async(flags: .barrier) { + self.set.removeAll() + } + } + + public func contains(_ member: T) -> Bool { + queue.sync { set.contains(member) } + } + + public func first(where predicate: (T) -> Bool) -> T? { + queue.sync { set.first(where: predicate) } + } +} + diff --git a/Tests/NGToolsTests/ThreadSafeSetTests.swift b/Tests/NGToolsTests/ThreadSafeSetTests.swift new file mode 100644 index 0000000..0fe356f --- /dev/null +++ b/Tests/NGToolsTests/ThreadSafeSetTests.swift @@ -0,0 +1,65 @@ +// +// ThreadSafeSetTests.swift +// +// +// Created by Jeanne Creantor, N'rick (Ordinateur) on 2024-06-04. +// + +import XCTest + +import NGTools + +final class ThreadSafeSetTests: XCTestCase { + + func testConcurrencyInsertionAndRemove() { + let set = ThreadSafeSet() + let expectation = XCTestExpectation(description: "Testing") + + let queueA = DispatchQueue(label: "TestQueueA", qos: .background) + let queueB = DispatchQueue(label: "TestQueueB", qos: .background) + let iterations = 1000 + let requests = Array(repeating: UUID(), count: iterations) + var completed = 0 + + requests.forEach { request in + queueA.async { set.insert(request) } + + queueB.async { + set.remove(request) + completed += 1 + if completed == iterations - 1 { + expectation.fulfill() + } + } + } + + wait(for: [expectation], timeout: 3) + } + + func testConcurrencyContainsAndRemove() { + let expectation = XCTestExpectation(description: "Testing") + + let queueA = DispatchQueue(label: "TestQueueA", qos: .background) + let queueB = DispatchQueue(label: "TestQueueB", qos: .background) + let iterations = 1000 + let requests = Array(repeating: UUID(), count: iterations) + let set = ThreadSafeSet(set: Set(requests)) + var completed = 0 + + requests.forEach { request in + queueA.async { + _ = set.contains(request) + } + + queueB.async { + set.remove(request) + completed += 1 + if completed == iterations - 1 { + expectation.fulfill() + } + } + } + + wait(for: [expectation], timeout: 3) + } +}