├── .codecov.yml ├── .github └── workflows │ └── Build.yml ├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── astemireleev.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist │ ├── xcshareddata │ └── xcschemes │ │ └── ConcurrencyKit.xcscheme │ └── xcuserdata │ └── astemireleev.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ConcurrencyKit │ ├── Array │ └── Array+ConcurrentMap.swift │ ├── Atomics │ ├── Atomic.swift │ ├── AtomicBool.swift │ └── AtomicInt.swift │ ├── Dispatch │ ├── DispatchQueue+AsyncAfter.swift │ └── DispatchQueue+Once.swift │ ├── Locks │ ├── LockType.swift │ ├── Mutex.swift │ ├── ReadWriteLock.swift │ └── UnfairLock.swift │ ├── Operations │ └── StatefullOperation.swift │ └── Task │ └── Task.swift ├── Tests ├── ConcurrencyKitTests │ ├── Array+ConcurrentMapTests.swift │ ├── AtomicBoolTests.swift │ ├── AtomicIntTests.swift │ ├── AtomicTests.swift │ ├── DispatchQueue+AsyncAfterTests.swift │ ├── DispatchQueue+OnceTests.swift │ ├── ReadWriteLockTests.swift │ ├── StatefullOperationTests.swift │ ├── TaskTests.swift │ ├── UnfairLockTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift └── logo-concurrency_kit.png /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | strict_yaml_branch: master 3 | 4 | ignore: 5 | - "concurrency-kitTests/.*" 6 | -------------------------------------------------------------------------------- /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macOS-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | 9 | - name: Switch to Xcode 11 10 | run: sudo xcode-select --switch /Applications/Xcode_11.3.app 11 | # Since we want to be running our tests from Xcode, we need to 12 | # generate an .xcodeproj file. Luckly, Swift Package Manager has 13 | # build in functionality to do so. 14 | - name: Generate xcodeproj 15 | run: swift package generate-xcodeproj 16 | - name: Run tests 17 | run: xcodebuild test -destination 'name=iPhone 11' -scheme 'ConcurrencyKit-Package' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/astemireleev.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/ConcurrencyKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 68 | 69 | 75 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/astemireleev.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ConcurrencyKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | ConcurrencyKit 16 | 17 | primary 18 | 19 | 20 | ConcurrencyKitTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Astemir Eleev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ConcurrencyKit", 8 | platforms: [ 9 | .iOS(.v12), 10 | .macOS(.v10_13), 11 | .watchOS(.v5) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "ConcurrencyKit", 17 | targets: ["ConcurrencyKit"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "ConcurrencyKit", 28 | dependencies: []), 29 | .testTarget( 30 | name: "ConcurrencyKitTests", 31 | dependencies: ["ConcurrencyKit"]), 32 | ], 33 | swiftLanguageVersions: [ 34 | .v5 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # concurrency-kit [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) 2 | 3 | [![Build](https://github.com/jvirus/concurrency-kit/workflows/Build/badge.svg)]() 4 | [![Coverage](https://codecov.io/gh/jVirus/concurrency-kit/branch/master/graph/badge.svg)](https://codecov.io/gh/jVirus/concurrency-kit) 5 | [![Platforms](https://img.shields.io/badge/Platforms-iOS/iPadOS/WatchOS/macOS-yellow.svg)]() 6 | [![Language](https://img.shields.io/badge/Language-Swift_5.1-orange.svg)]() 7 | [![SPM](https://img.shields.io/badge/SPM-Supported-red.svg)]() 8 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)]() 9 | 10 | **Last Update: 05/January/2020.** 11 | 12 | ![](logo-concurrency_kit.png) 13 | 14 | ### If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo. 15 | 16 | # ✍️ About 17 | 🚄 Concurrency abstractions framework for `iOS` development [`Task`, `Atomic`, `Lock`, etc.]. 18 | 19 | # 🔥 Features 20 | - **Atomics** - synchronization primitive that is implemented in several forms: `Generic`, `Int` and `Bool`. 21 | - `Fast`. Under the hood a mutex (`pthread_mutex_lock`) that is more efficient than `OSSpinLock` and faster than `NSLock`. 22 | - `Throwable`. You can safely throw `Errors` and be able to delegate the handling. 23 | - **Locks** - contains a number of locks, such as: 24 | - `UnfairLock` - A lock which causes a thread trying to acquire it to simply wait in a loop ("spin") while repeatedly checking if the lock is available. 25 | - `ReadWriteLock` - An `RW` lock allows concurrent access for read-only operations, while write operations require exclusive access. 26 | - `Mutex` - Allows only one thread to be active in a given region of code. 27 | - **DispatchQueue+Extensions** - extended `DispatchQueue`, where `asyncAfter` and `once` methods add convenience. 28 | - **Task** - A unit of work that performs a specific job and usually runs concurrently with other tasks. 29 | - Tasks can be `grouped` - meaning that you are able to compose the tasks, similar to `Futures & Promises` and execute them serially. 30 | - Tasks can be `sequenced` - meaning that you are able to compose different `groups` and execute them concurrently. No need to repeatedly use `DispatchGroup` (`enter`/`leave`). 31 | - **Stateful Operation** - is a custom `Operation` class that supports modern, `Swifty` state management through the usage of `Atomics` and `Enum` types. 32 | - **Thoroughly** tested. 33 | 34 | # 📚 Examples 35 | 36 | ## Task 37 | In order to create a `Task`, you need to simply use the `Task` struct and the `trailing closure` syntax: 38 | 39 | ```swift 40 | let uploadingTask = Task { controller in 41 | uploader(photos) { result in 42 | switch result { 43 | case .success: 44 | controller.finish() 45 | case .failure(let error): 46 | controller.fail(with error) 47 | } 48 | } 49 | } 50 | 51 | uploadingTask.perform { outcome in 52 | handle(outcome) 53 | } 54 | ``` 55 | 56 | You can group the tasks, so the concurrent operations will be performed sequentially, one after another. Then, you can chain a completion closure to handle the outcome: 57 | 58 | ```swift 59 | let filesToUpload = [file, photo, video, xml] 60 | 61 | let group = Task.group(fileToUpload) 62 | group.perform { outcome in 63 | handle(outcome) 64 | } 65 | ``` 66 | 67 | Or you can concurrently perform a collection of tasks. They will be executed asynchronously, in parallel (if possible) or concurrently, that is up to the `GCD`: 68 | 69 | ```swift 70 | let filesToUpload = [file, photo, video, xml] 71 | 72 | let group = Task.sequence(filesToUpload) 73 | group.perform { outcome in 74 | handle(outcome) 75 | } 76 | ``` 77 | 78 | ## Stateful Operation 79 | Operation that has more 'Swifty' state management system, where state is an enum type with a number of possible cases. In order to demostrate the typical usage, let's define a new custom operation for network request: 80 | 81 | ```swift 82 | class NetworkStatefulOperation: StatefullOperation { 83 | 84 | // MARK: - Properties 85 | 86 | private let callback: (StatefullOperation?) -> Void 87 | private let service: NetworkService 88 | private let dataHandler: Parsable 89 | 90 | // MARK: - Initializers 91 | 92 | init(_ service: NetworkService, _ dataHandler: Parser, callback: @escaping (StatefullOperation?) -> Void) { 93 | self.service = service 94 | self.dataHandler = dataHandler 95 | self.callback = callback 96 | } 97 | 98 | // MARK: - Overrides 99 | 100 | override func executableSection() { 101 | service.dataTask { [weak self] result in 102 | self?.dataHandler.parse(result) 103 | self?.finishIfNotCancelled() 104 | self?.callback(self) 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | Then, the usage of the `NetworkStatefulOperation` class is quite straightforward: 111 | 112 | ```swift 113 | // 1. Create an instance of `NetworkStatefulOperation` class: 114 | let networkiOperation = NetworkStatefulOperation(service, parser) { 115 | // 3. As soon as the operation is finished, this closure will be executed with the operation state that can futher be handled to properly update the UI: 116 | updateUI(with: $0.state) 117 | } 118 | // 2. Then call the `start` method: 119 | networkOperation.start() 120 | ``` 121 | 122 | ## Atomics 123 | Guarantees safe mutation of a property in multiple async dispatch queues. Simply wrap a property in `Atomic` type: 124 | 125 | ```swift 126 | let atomic = Atomic(0) 127 | 128 | DispatchQueue.global().async { 129 | atomic.modify { $0 + 1 } 130 | } 131 | 132 | DispatchQueue.global().async { 133 | atomic.modify { $0 + 1 } 134 | } 135 | ``` 136 | 137 | You can also use slightly more performance-friendly `AtomicInt` and `AtomicBool` classes, hence there is no dynamic dispatch involved (though `Swift` compiler is smart enough to apply complier optimization called [`compile-time generic specialization`](https://forums.swift.org/t/compile-time-generic-specialization/5082)) 138 | 139 | ## Locks 140 | 141 | ### Read Write Lock 142 | A syncronozation primitive that solves one of the readers–writers problems: 143 | 144 | ```swift 145 | let rwLock = ReadWriteLock() 146 | 147 | rwLock.writeLock() 148 | sharedValue += 1 149 | rwLock.unlock() 150 | ``` 151 | 152 | Or you can restrict the reading access, so other threads will not be able to read the mutated value of a property until the lock will be released: 153 | 154 | ```swift 155 | let rwLock = ReadWriteLock() 156 | 157 | rwLock.readLock() 158 | sharedValue += 1 159 | rwLock.unlock() 160 | ``` 161 | 162 | ### Unfair Lock 163 | A lock which causes a thread trying to acquire it to simply wait in a loop ("spin"), while repeatedly checking if the lock is available: 164 | 165 | ```swift 166 | let unfairLock = UnfairLock() 167 | 168 | unfairLock.lock() 169 | sharedValue += 1 170 | unfairLock.unlock() 171 | ``` 172 | 173 | ### Mutex 174 | Used to protect shared resources. A mutex is owned by the task that takes it. In a given region of code only one thread is active: 175 | 176 | ```swift 177 | let mutex = Mutex() 178 | 179 | mutex.withCriticalScope { 180 | return sharedValue += 1 181 | } 182 | ``` 183 | 184 | ## Dispatch Queue 185 | There is a convenience method that removes the need to pass `.now() + time` in order to make an async call: 186 | 187 | ```swift 188 | DispatchQueue.main.asyncAfter(seconds: 2.5) { 189 | expectation.fulfill() 190 | } 191 | ``` 192 | 193 | Also, `DispatchQueue.once` was returned back:: 194 | 195 | ```swift 196 | // The following concurrentQueue is called multiple times, though the caughtValue will be set to value only once. 197 | concurrentQueue.async { 198 | DispatchQueue.once(token: "caught") { 199 | caughtValue = value 200 | } 201 | } 202 | ``` 203 | 204 | # 🏗 Installation 205 | 206 | ## Swift Package Manager 207 | 208 | ### Xcode 11+ 209 | 210 | 1. Open `MenuBar` → `File` → `Swift Packages` → `Add Package Dependency...` 211 | 2. Paste the package repository url `https://github.com/jVirus/concurrency-kit` and hit `Next`. 212 | 3. Select the installment rules. 213 | 214 | After specifying which version do you want to install, the package will be downloaded and attached to your project. 215 | 216 | ### Package.swift 217 | If you already have a `Package.swift` or you are building your own package simply add a new dependency: 218 | 219 | ```swift 220 | dependencies: [ 221 | .package(url: "https://github.com/jVirus/concurrency-kit", from: "1.0.0") 222 | ] 223 | ``` 224 | 225 | ## Manual 226 | You can always use copy-paste the sources method 😄. Or you can compile the framework and include it with your project. 227 | 228 | 229 | 230 | # 👨‍💻 Author 231 | [Astemir Eleev](https://github.com/jVirus) 232 | 233 | # 🔖 Licence 234 | The project is available under [MIT licence](https://github.com/jVirus/concurrency-kit/blob/master/LICENSE) 235 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Array/Array+ConcurrentMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+ConcurrentMap.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 02/06/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | 13 | /// Concurrently performs map function for the given collection of elements 14 | public func concurrentMap(_ transform: @escaping (Element) -> E) -> [E?] { 15 | var result = Array(repeating: nil, count: count) 16 | let queue = DispatchQueue(label: "array.concurrent.map") 17 | 18 | DispatchQueue.concurrentPerform(iterations: count) { index in 19 | let element = self[index] 20 | let transformed = transform(element) 21 | queue.sync { 22 | result[index] = transformed 23 | } 24 | } 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Atomics/Atomic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Atomic.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Guarantees that a valid value will be returned when accessing such property by using multiple thread. The valid does not always mean correct (atomic property may not be in the state that you expect, at a time when you access it). 12 | final public class Atomic { 13 | 14 | // MARK: - Properties 15 | 16 | internal private(set) var mutex: Mutex 17 | private(set) var value: T 18 | 19 | // MARK: - Initialzers 20 | 21 | public init(_ value: T) { 22 | mutex = Mutex() 23 | self.value = value 24 | } 25 | 26 | // MARK: - Methods 27 | 28 | public func set(_ value: T) { 29 | mutex.withCriticalScope { [weak self] in 30 | self?.value = value 31 | } 32 | } 33 | 34 | public func get() -> T { 35 | return mutex.withCriticalScope { value } 36 | } 37 | 38 | public func swap(_ value: T) -> T { 39 | return modify { _ in value } 40 | } 41 | 42 | @discardableResult 43 | public func with(_ closure: (T) throws -> R) rethrows -> R { 44 | return try mutex.withCriticalScope { 45 | try closure(value) 46 | } 47 | } 48 | 49 | @discardableResult 50 | public func modify(_ action: (T) throws -> T) rethrows -> T { 51 | return try mutex.withCriticalScope { 52 | let previous = value 53 | value = try action(value) 54 | return previous 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Atomics/AtomicBool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicBool.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final public class AtomicBool { 12 | 13 | // MARK: - Properties 14 | 15 | internal private(set) var mutex: Mutex 16 | private(set) var value: Bool 17 | 18 | // MARK: - Initialzers 19 | 20 | public init(_ value: Bool) { 21 | mutex = Mutex() 22 | self.value = value 23 | } 24 | 25 | // MARK: - Methods 26 | 27 | public func set(_ value: Bool) { 28 | mutex.withCriticalScope { [weak self] in 29 | self?.value = value 30 | } 31 | } 32 | 33 | public func get() -> Bool { 34 | return mutex.withCriticalScope { value } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Atomics/AtomicInt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicInt.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final public class AtomicInt { 12 | 13 | // MARK: - Properties 14 | 15 | internal private(set) var mutex: Mutex 16 | private(set) var value: Int 17 | 18 | // MARK: - Initialzers 19 | 20 | public init(_ value: Int) { 21 | mutex = Mutex() 22 | self.value = value 23 | } 24 | 25 | // MARK: - Methods 26 | 27 | public func set(_ value: Int) { 28 | mutex.withCriticalScope { [weak self] in 29 | self?.value = value 30 | } 31 | } 32 | 33 | public func get() -> Int { 34 | return mutex.withCriticalScope { value } 35 | } 36 | 37 | public func swap(_ value: Int) -> Int { 38 | return modify { _ in value } 39 | } 40 | 41 | @discardableResult 42 | public func with(_ closure: (Int) throws -> Int) rethrows -> Int { 43 | return try mutex.withCriticalScope { 44 | try closure(value) 45 | } 46 | } 47 | 48 | @discardableResult 49 | public func modify(_ action: (Int) throws -> Int) rethrows -> Int { 50 | return try mutex.withCriticalScope { 51 | let previous = value 52 | value = try action(value) 53 | return previous 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Dispatch/DispatchQueue+AsyncAfter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+AsyncAfter.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension DispatchQueue { 12 | 13 | func asyncAfter(seconds: Double, execute closure: @escaping () -> ()) { 14 | let secPerSec = Double(NSEC_PER_SEC) 15 | let deadline: DispatchTime = DispatchTime.now() + Double(Int64(seconds * secPerSec)) / secPerSec 16 | self.asyncAfter(deadline: deadline, execute: closure) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Dispatch/DispatchQueue+Once.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+Once.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension DispatchQueue { 12 | 13 | // MARK: - Properties 14 | 15 | private static var _onceTracker = [String]() 16 | 17 | // MARK: - Methods 18 | 19 | /// Executes a block of code, associated with a unique token, only once. The code is thread safe and will onle execute the code once even in the presence of multithreaded calls. 20 | /// 21 | /// - Parameters: 22 | /// - token: is a unique reverse DNS-style name such as io.eleev.astemir or a GUID 23 | /// - block: is a non-escaping closure that is executed only once 24 | class func once(token: String, block: () -> Void) { 25 | objc_sync_enter(self) 26 | defer { objc_sync_exit(self) } 27 | 28 | if _onceTracker.contains(token) { return } 29 | _onceTracker.append(token) 30 | block() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Locks/LockType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockType.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol LockType { 12 | func lock() 13 | func unlock() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Locks/Mutex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mutex.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Used to protect shared resources. A mutex is owned by the task that takes it. In a given region of code only one thread is active. 12 | final public class Mutex: LockType { 13 | 14 | // MARK: - Properties 15 | 16 | private var mutex = pthread_mutex_t() 17 | 18 | // MARK: - Init & Deinit 19 | 20 | init() { 21 | let result = pthread_mutex_init(&mutex, nil) 22 | assert(result == 0, "Failed to init mutex in \(self)") 23 | } 24 | 25 | deinit { 26 | destroy() 27 | } 28 | 29 | // MARK: - Methods 30 | 31 | @discardableResult 32 | public func tryLock() -> Int32 { 33 | return pthread_mutex_trylock(&mutex) 34 | } 35 | 36 | // MARK: - Conformance to LockType protocol 37 | 38 | public func lock() { 39 | pthread_mutex_lock(&mutex) 40 | } 41 | 42 | public func unlock() { 43 | pthread_mutex_unlock(&mutex) 44 | } 45 | 46 | // MARK: - Private methods 47 | 48 | private func destroy() { 49 | pthread_mutex_destroy(&mutex) 50 | } 51 | } 52 | 53 | public extension Mutex { 54 | func withCriticalScope(body: () throws -> R) rethrows -> R { 55 | lock() 56 | defer { unlock() } 57 | return try body() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Locks/ReadWriteLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadWriteLock.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A synchronization primitive that solves one of the readers–writers problems. An RW lock allows concurrent access for read-only operations, while write operations require exclusive access. This means that multiple threads can read the data in parallel but an exclusive lock is needed for writing or modifying data. 12 | final public class ReadWriteLock { 13 | 14 | // MARK: - Properties 15 | 16 | private var rwLock = pthread_rwlock_t() 17 | 18 | // MARK: - Init & Deinit 19 | 20 | public init() { 21 | let result = pthread_rwlock_init(&rwLock, nil) 22 | assert(result == 0, "Failed to init read write lock in \(self)") 23 | } 24 | 25 | deinit { 26 | destroy() 27 | } 28 | 29 | // MARK: - Methods 30 | 31 | public func readLock() { 32 | pthread_rwlock_rdlock(&rwLock) 33 | } 34 | 35 | @discardableResult 36 | public func tryReadLock() -> Int32 { 37 | return pthread_rwlock_tryrdlock(&rwLock) 38 | } 39 | 40 | public func writeLock() { 41 | pthread_rwlock_wrlock(&rwLock) 42 | } 43 | 44 | @discardableResult 45 | public func tryWriteLock() -> Int32 { 46 | return pthread_rwlock_trywrlock(&rwLock) 47 | } 48 | 49 | public func unlock() { 50 | pthread_rwlock_unlock(&rwLock) 51 | } 52 | 53 | // MARK: - Private methods 54 | 55 | private func destroy() { 56 | pthread_rwlock_destroy(&rwLock) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Locks/UnfairLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnfairLock.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A lock which causes a thread trying to acquire it to simply wait in a loop ("spin") while repeatedly checking if the lock is available 12 | final public class UnfairLock: LockType { 13 | 14 | // MARK: - Properties 15 | 16 | private var unfairLock = os_unfair_lock() 17 | 18 | // MARK: - Conformance to LockType protocol 19 | 20 | public func lock() { 21 | os_unfair_lock_lock(&unfairLock) 22 | } 23 | 24 | @discardableResult 25 | public func tryLock() -> Bool { 26 | return os_unfair_lock_trylock(&unfairLock) 27 | } 28 | 29 | public func unlock() { 30 | os_unfair_lock_unlock(&unfairLock) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Operations/StatefullOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatefulOperation.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 14/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Operation that has more 'Swifty' state management system, where state is an enum type with a number of possible cases: 12 | /// - `ready` - an operation is initialized and is ready to be executed 13 | /// - `executing` - an operation is performing a work 14 | /// - `cancelled` - an operation has not yet successfully finished the work, but was interrupted 15 | /// - `finished` - an operation has successfully finished the work 16 | /// 17 | /// In order to use the class, you need to subclass it and override the `executableSection` method, where the work need to be done. When the work is done, you need to call the `finish` or `finishIfNotCancelled` methods in order to properly change the internal state of an operation. 18 | open class StatefulOperation: Operation { 19 | 20 | // MARK: - Enum types 21 | 22 | public enum State: Equatable { 23 | 24 | // MARK: - Cases 25 | 26 | case ready 27 | case executing 28 | case cancelled 29 | case finished 30 | 31 | // MARK: - Fileprivate properties 32 | 33 | fileprivate var key: String { 34 | switch self { 35 | case .ready: 36 | return "isReady" 37 | case .executing: 38 | return "isExecuting" 39 | case .cancelled: 40 | return "isCancelled" 41 | case .finished: 42 | return "isFinished" 43 | } 44 | } 45 | } 46 | 47 | // MARK: - Private properties 48 | 49 | private let queue = DispatchQueue(label: "io.eleev.astemir.concurrency-kit.statefull-operation") 50 | private var _state: Atomic = Atomic(.ready) 51 | 52 | // MARK: - Public properties 53 | 54 | public private(set) var state: State { 55 | get { 56 | return _state.get() 57 | } 58 | set { 59 | queue.sync { 60 | let oldValue = _state.get() 61 | guard newValue != oldValue else { return } 62 | 63 | willChangeValue(forKey: oldValue.key) 64 | willChangeValue(forKey: newValue.key) 65 | 66 | _state.set(newValue) 67 | 68 | didChangeValue(forKey: oldValue.key) 69 | didChangeValue(forKey: newValue.key) 70 | } 71 | } 72 | } 73 | 74 | // MARK: - Overriden properties 75 | 76 | final public override var isAsynchronous: Bool { 77 | return true 78 | } 79 | 80 | final public override var isExecuting: Bool { 81 | return state == .executing 82 | } 83 | 84 | final public override var isFinished: Bool { 85 | return state == .finished 86 | } 87 | 88 | final public override var isCancelled: Bool { 89 | return state == .cancelled 90 | } 91 | 92 | // MARK: - Overriden methods 93 | 94 | final public override func start() { 95 | if isCancelled { interrupt(); return } 96 | if isFinished { finish(); return } 97 | main() 98 | } 99 | 100 | final public override func main() { 101 | if isCancelled { interrupt(); return } 102 | if isFinished { finish(); return } 103 | state = .executing 104 | executableSection() 105 | } 106 | 107 | // MARK: - Methods 108 | 109 | final public func interrupt() { 110 | state = .cancelled 111 | cancel() 112 | } 113 | 114 | final public func finish() { 115 | state = .finished 116 | } 117 | 118 | final public func finishIfNotCancelled() { 119 | if state != .cancelled { finish() } 120 | } 121 | 122 | /// Overridable point, that is required to call `finish` method in order to mark the executable section as finished. 123 | open func executableSection() { 124 | finish() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/ConcurrencyKit/Task/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // ConcurrencyKit 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Task { 12 | 13 | // MARK: - Typealiases 14 | 15 | public typealias Closure = (Controller) -> Void 16 | 17 | // MARK: - Properties 18 | 19 | private let closure: Closure 20 | 21 | // MARK: - Initializers 22 | 23 | public init(closure: @escaping Closure) { 24 | self.closure = closure 25 | } 26 | 27 | // MARK: - Methods 28 | 29 | public func perform(on queue: DispatchQueue = .global(), 30 | then handler: @escaping (Outcome) -> Void) { 31 | queue.async { 32 | let controller = Controller( 33 | queue: queue, 34 | handler: handler 35 | ) 36 | 37 | self.closure(controller) 38 | } 39 | } 40 | } 41 | 42 | 43 | // MARK: - Constroller extension 44 | public extension Task { 45 | 46 | // MARK: - Controller struct 47 | 48 | struct Controller { 49 | 50 | // MARK: - Priperties 51 | 52 | fileprivate let queue: DispatchQueue 53 | fileprivate let handler: (Outcome) -> Void 54 | 55 | // MARK: - Methods 56 | 57 | public func finish() { 58 | handler(.success) 59 | } 60 | 61 | public func fail(with error: Error) { 62 | handler(.failure(error)) 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Outcome extension 68 | public extension Task { 69 | 70 | // MARK: - Enum type 71 | 72 | enum Outcome: Equatable { 73 | 74 | // MARK: - Cases 75 | 76 | case success 77 | case failure(Swift.Error) 78 | 79 | // MARK: - Conformance to Equatable protocol 80 | 81 | public static func == (lhs: Task.Outcome, rhs: Task.Outcome) -> Bool { 82 | switch (lhs, rhs) { 83 | case (.success, .success): 84 | return true 85 | case (.failure( _), .failure( _)): 86 | return true 87 | default: 88 | return false 89 | } 90 | } 91 | } 92 | } 93 | 94 | // MARK: - Serial task executor 95 | public extension Task { 96 | 97 | static func group(_ tasks: [Task]) -> Task { 98 | return Task { controller in 99 | let group = DispatchGroup() 100 | let errorSyncQueue = DispatchQueue(label: "Task.Error.Sync") 101 | var anyError: Error? 102 | 103 | for task in tasks { 104 | group.enter() 105 | task.perform(on: controller.queue) { outcome in 106 | switch outcome { 107 | case .success: 108 | break 109 | case .failure(let error): 110 | errorSyncQueue.sync { 111 | anyError = anyError ?? error 112 | } 113 | } 114 | 115 | group.leave() 116 | } 117 | } 118 | 119 | group.notify(queue: controller.queue) { 120 | if let error = anyError { 121 | controller.fail(with: error) 122 | } else { 123 | controller.finish() 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | // MARK: - Concurrent task executor 131 | public extension Task { 132 | 133 | static func sequence(_ tasks: [Task]) -> Task { 134 | var index = 0 135 | 136 | func performNext(using controller: Controller) { 137 | guard index < tasks.count else { 138 | controller.finish() 139 | return 140 | } 141 | 142 | let task = tasks[index] 143 | index += 1 144 | 145 | task.perform(on: controller.queue) { outcome in 146 | switch outcome { 147 | case .success: 148 | performNext(using: controller) 149 | case .failure(let error): 150 | // As soon as an error was occurred, we’ll 151 | // fail the entire sequence. 152 | controller.fail(with: error) 153 | } 154 | } 155 | } 156 | 157 | return Task(closure: performNext) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/Array+ConcurrentMapTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+ConcurrentMapTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 02/06/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | final class ArrayConcurrentMapTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("test", test) 16 | ] 17 | 18 | override func setUp() { 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func test() { 27 | let elements = Array(repeating: 5, count: 100000) 28 | 29 | measure { 30 | let _ = elements.concurrentMap { element -> Int in 31 | return element * element 32 | } 33 | } 34 | 35 | // measure { 36 | // var output = [elements.count] 37 | // 38 | // for (index, element) in elements.enumerated() { 39 | // let result = element * index 40 | // output.append(result) 41 | // } 42 | // } 43 | } 44 | 45 | func testPerformanceExample() { 46 | // This is an example of a performance test case. 47 | self.measure { 48 | // Put the code you want to measure the time of here. 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/AtomicBoolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicBoolTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class AtomicBoolTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("testSet", testSet), 16 | ("testGet", testGet) 17 | ] 18 | 19 | override func setUp() { 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | override func tearDown() { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func testSet() { 28 | let atomic = AtomicBool(true) 29 | atomic.set(false) 30 | XCTAssertEqual(atomic.value, false) 31 | } 32 | 33 | func testGet() { 34 | let atomic = AtomicBool(false) 35 | atomic.set(true) 36 | let value = atomic.get() 37 | XCTAssertEqual(value, true) 38 | } 39 | 40 | func testPerformanceExample() { 41 | // This is an example of a performance test case. 42 | self.measure { 43 | // Put the code you want to measure the time of here. 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/AtomicIntTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicIntTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class AtomicIntTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("testModify", testModify), 16 | ("testWith", testWith), 17 | ("testSet", testSet), 18 | ("testGet", testGet), 19 | ("testSwap", testSwap), 20 | ("testRethrowFromWithValue", testRethrowFromWithValue), 21 | ("testRethrowFromModify", testRethrowFromModify), 22 | ("testHighlyContestedLocking", testHighlyContestedLocking) 23 | ] 24 | 25 | override func setUp() { 26 | // Put setup code here. This method is called before the invocation of each test method in the class. 27 | } 28 | 29 | override func tearDown() { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | } 32 | 33 | func testModify() { 34 | let atomic = AtomicInt(0) 35 | atomic.modify { $0 + 10 } 36 | XCTAssertEqual(atomic.value, 10) 37 | } 38 | 39 | func testWith() { 40 | let atomic = AtomicInt(0) 41 | let result = atomic.with { $0 + 10 } 42 | XCTAssertEqual(atomic.value, 0) 43 | XCTAssertEqual(result, 10) 44 | } 45 | 46 | func testSet() { 47 | let atomic = AtomicInt(0) 48 | atomic.set(10) 49 | XCTAssertEqual(atomic.value, 10) 50 | } 51 | 52 | func testGet() { 53 | let atomic = AtomicInt(0) 54 | atomic.set(30) 55 | XCTAssertEqual(atomic.value, 30) 56 | } 57 | 58 | func testSwap() { 59 | let atomic = AtomicInt(1) 60 | let oldValue = atomic.swap(10) 61 | XCTAssertEqual(oldValue, 1) 62 | XCTAssertEqual(atomic.value, 10) 63 | } 64 | 65 | func testRethrowFromWithValue() { 66 | let atomic = AtomicInt(5) 67 | var didCatch = false 68 | 69 | do { 70 | try atomic.with { value in 71 | throw NSError(domain: NSCocoaErrorDomain, code: value, userInfo: nil) 72 | } 73 | XCTFail() 74 | } catch let error as NSError { 75 | didCatch = true 76 | XCTAssertEqual(error, NSError(domain: NSCocoaErrorDomain, code: 5, userInfo: nil)) 77 | } 78 | 79 | if atomic.mutex.tryLock() == 0 { 80 | atomic.mutex.unlock() 81 | } else { 82 | XCTFail() 83 | } 84 | XCTAssert(didCatch) 85 | } 86 | 87 | func testRethrowFromModify() { 88 | let atomic = AtomicInt(5) 89 | var didCatch = false 90 | 91 | do { 92 | try atomic.modify { value in 93 | throw NSError(domain: NSCocoaErrorDomain, code: value, userInfo: nil) 94 | } 95 | XCTFail() 96 | } catch let error as NSError { 97 | didCatch = true 98 | XCTAssertEqual(error, NSError(domain: NSCocoaErrorDomain, code: 5, userInfo: nil)) 99 | } 100 | 101 | if atomic.mutex.tryLock() == 0 { 102 | atomic.mutex.unlock() 103 | } else { 104 | XCTFail() 105 | } 106 | XCTAssert(didCatch) 107 | } 108 | 109 | func testHighlyContestedLocking() { 110 | let contestedAtomic = AtomicInt(0) 111 | 112 | let concurrentQueue = DispatchQueue.global() 113 | let dispatchGroup = DispatchGroup() 114 | let count = 10_000 115 | 116 | for _ in 0.. 2.45 && timeInterval < 2.6) 37 | 38 | expectation.fulfill() 39 | } 40 | 41 | wait(for: [expectation], timeout: 4.0) 42 | } 43 | 44 | func testPerformanceExample() { 45 | // This is an example of a performance test case. 46 | self.measure { 47 | // Put the code you want to measure the time of here. 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/DispatchQueue+OnceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchQueue+OnceTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class DispatchQueue_OnceTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("test", test) 16 | ] 17 | 18 | override func setUp() { 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func test() { 27 | var occurences = 0 28 | let dispatchQueue = DispatchQueue.global() 29 | let dispatchGroup = DispatchGroup() 30 | 31 | for _ in 0..<100 { 32 | dispatchGroup.enter() 33 | dispatchQueue.async { 34 | DispatchQueue.once(token: "Unique Id") { 35 | occurences += 1 36 | } 37 | dispatchGroup.leave() 38 | } 39 | } 40 | let extation = expectation(description: "Dispatch Group Completion") 41 | dispatchGroup.notify(queue: dispatchQueue) { 42 | extation.fulfill() 43 | } 44 | waitForExpectations(timeout: 5, handler: nil) 45 | XCTAssertEqual(occurences, 1) 46 | } 47 | 48 | func testPerformanceExample() { 49 | // This is an example of a performance test case. 50 | self.measure { 51 | // Put the code you want to measure the time of here. 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/ReadWriteLockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadWriteLockTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class ReadWriteLockTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("testReadLock", testReadLock), 16 | ("testWriteLock", testWriteLock) 17 | ] 18 | 19 | override func setUp() { 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | override func tearDown() { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | } 26 | 27 | func testReadLock() { 28 | var value = 0 29 | let lock = ReadWriteLock() 30 | 31 | let concurrentQueue = DispatchQueue.global() 32 | let anotherConcurrentQueue = DispatchQueue.global() 33 | let dispatchGroup = DispatchGroup() 34 | let anotherDispatchGroup = DispatchGroup() 35 | let count = 10_000 36 | 37 | lock.readLock() 38 | for _ in 0.. Void 31 | 32 | init(callback: @escaping (StatefulOperation) -> Void) { 33 | self.callback = callback 34 | } 35 | 36 | override func executableSection() { 37 | DispatchQueue.global().async { 38 | sleep(3) 39 | self.finish() 40 | self.callback(self) 41 | } 42 | } 43 | } 44 | 45 | 46 | let expectation = XCTestExpectation(description: "Operation Expectation") 47 | 48 | let statefullOperation = MockStatefullOperation { 49 | XCTAssertEqual($0.state, .finished) 50 | expectation.fulfill() 51 | } 52 | 53 | XCTAssertEqual(statefullOperation.state, .ready) 54 | statefullOperation.start() 55 | 56 | XCTAssertEqual(statefullOperation.state, .executing) 57 | 58 | wait(for: [expectation], timeout: 5.0) 59 | } 60 | 61 | func testInterruptedOperation() { 62 | class MockStatefullOperation: StatefulOperation { 63 | 64 | private var callback: (StatefulOperation) -> Void 65 | 66 | init(callback: @escaping (StatefulOperation) -> Void) { 67 | self.callback = callback 68 | } 69 | 70 | override func executableSection() { 71 | DispatchQueue.global().async { 72 | sleep(3) 73 | 74 | self.finishIfNotCancelled() 75 | self.callback(self) 76 | } 77 | } 78 | } 79 | 80 | 81 | let expectation = XCTestExpectation(description: "Operation Expectation") 82 | 83 | let statefullOperation = MockStatefullOperation { 84 | XCTAssertEqual($0.state, .cancelled) 85 | expectation.fulfill() 86 | } 87 | 88 | XCTAssertEqual(statefullOperation.state, .ready) 89 | statefullOperation.start() 90 | XCTAssertEqual(statefullOperation.state, .executing) 91 | 92 | // Then after 0.75 seconds the operation by some reason is interrupted and it was decided to cancel it 93 | DispatchQueue.main.asyncAfter(seconds: 0.75) { 94 | statefullOperation.interrupt() 95 | 96 | XCTAssertEqual(statefullOperation.state, .cancelled) 97 | XCTAssertEqual(statefullOperation.isCancelled, true) 98 | } 99 | 100 | wait(for: [expectation], timeout: 5.0) 101 | } 102 | 103 | func testPerformanceExample() { 104 | // This is an example of a performance test case. 105 | self.measure { 106 | // Put the code you want to measure the time of here. 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/TaskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class TaskTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("testPerform", testPerform), 16 | ("testGroup", testGroup), 17 | ("testSequence", testSequence) 18 | ] 19 | 20 | override func setUp() { 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | override func tearDown() { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | } 27 | 28 | func testPerform() { 29 | let expectation = XCTestExpectation(description: "Task Expectation") 30 | 31 | let iterationTask = Task { controller in 32 | DispatchQueue.global().async { 33 | (0...10_000).forEach({ 34 | let _ = $0 * $0 35 | }) 36 | controller.finish() 37 | } 38 | } 39 | 40 | iterationTask.perform { outcome in 41 | XCTAssertEqual(outcome, Task.Outcome.success) 42 | expectation.fulfill() 43 | } 44 | 45 | wait(for: [expectation], timeout: 5.0) 46 | } 47 | 48 | func testGroup() { 49 | let expectation = XCTestExpectation(description: "Task Expectation") 50 | 51 | func longRunningTaskMock(completion: @escaping () -> Void) { 52 | DispatchQueue.global().async { 53 | (0...10_000).forEach({ 54 | let _ = $0 * $0 55 | }) 56 | completion() 57 | } 58 | } 59 | 60 | let longRunningOperations = [longRunningTaskMock, longRunningTaskMock, longRunningTaskMock, longRunningTaskMock, longRunningTaskMock] 61 | 62 | let anotherSetOfLongRunningOperations = longRunningOperations 63 | 64 | let longRunningTasks = longRunningOperations.map { operation in 65 | Task.init(closure: { controller in 66 | operation() { 67 | controller.finish() 68 | } 69 | }) 70 | } 71 | 72 | let anotherSeOfLongRunningTasks = anotherSetOfLongRunningOperations.map { operation in 73 | Task.init(closure: { controller in 74 | operation() { 75 | controller.finish() 76 | } 77 | }) 78 | } 79 | 80 | let group = Task.group(longRunningTasks + anotherSeOfLongRunningTasks) 81 | group.perform { outcome in 82 | XCTAssertEqual(outcome, Task.Outcome.success) 83 | expectation.fulfill() 84 | } 85 | 86 | wait(for: [expectation], timeout: 8.0) 87 | } 88 | 89 | func testSequence() { 90 | let expectation = XCTestExpectation(description: "Task Expectation") 91 | 92 | func longRunningTaskMock(completion: @escaping () -> Void) { 93 | DispatchQueue.global().async { 94 | (0...10_000).forEach({ 95 | let _ = $0 * $0 96 | }) 97 | completion() 98 | } 99 | } 100 | 101 | let longRunningOperations = [longRunningTaskMock, longRunningTaskMock, longRunningTaskMock, longRunningTaskMock, longRunningTaskMock] 102 | 103 | let anotherSetOfLongRunningOperations = longRunningOperations 104 | 105 | let longRunningTasks = longRunningOperations.map { operation in 106 | Task.init(closure: { controller in 107 | operation() { 108 | controller.finish() 109 | } 110 | }) 111 | } 112 | 113 | let anotherSeOfLongRunningTasks = anotherSetOfLongRunningOperations.map { operation in 114 | Task.init(closure: { controller in 115 | operation() { 116 | controller.finish() 117 | } 118 | }) 119 | } 120 | 121 | let group = Task.sequence(longRunningTasks + anotherSeOfLongRunningTasks) 122 | group.perform { outcome in 123 | XCTAssertEqual(outcome, Task.Outcome.success) 124 | expectation.fulfill() 125 | } 126 | 127 | wait(for: [expectation], timeout: 8.0) 128 | } 129 | 130 | 131 | func testPerformanceExample() { 132 | // This is an example of a performance test case. 133 | self.measure { 134 | // Put the code you want to measure the time of here. 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /Tests/ConcurrencyKitTests/UnfairLockTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnfairLockTests.swift 3 | // ConcurrencyKitTests 4 | // 5 | // Created by Astemir Eleev on 10/03/2019. 6 | // Copyright © 2019 Astemir Eleev. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ConcurrencyKit 11 | 12 | class UnfairLockTests: XCTestCase { 13 | 14 | static var allTests = [ 15 | ("testModify", testModify), 16 | ("testTryLock", testTryLock), 17 | ("testAsync", testAsync), 18 | ("testAsyncTwoQueues", testAsyncTwoQueues), 19 | ] 20 | 21 | override func setUp() { 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testModify() { 30 | let lock = UnfairLock() 31 | var value = 0 32 | 33 | lock.lock() 34 | value = 10 35 | lock.unlock() 36 | XCTAssertEqual(value, 10) 37 | } 38 | 39 | func testTryLock() { 40 | let lock = UnfairLock() 41 | var value = 0 42 | 43 | lock.tryLock() 44 | value = 10 45 | lock.unlock() 46 | XCTAssertEqual(value, 10) 47 | } 48 | 49 | func testAsync() { 50 | var value = 0 51 | let lock = UnfairLock() 52 | 53 | let concurrentQueue = DispatchQueue.global() 54 | let dispatchGroup = DispatchGroup() 55 | let count = 10_000 56 | 57 | for _ in 0.. [XCTestCaseEntry] { 5 | return [ 6 | testCase(ArrayConcurrentMapTests.allTests), 7 | testCase(AtomicBoolTests.allTests), 8 | testCase(AtomicIntTests.allTests), 9 | testCase(AtomicTests.allTests), 10 | testCase(DispatchQueue_AsyncAfterTests.allTests), 11 | testCase(DispatchQueue_OnceTests.allTests), 12 | testCase(ReadWriteLockTests.allTests), 13 | testCase(StatefullOperationTests.allTests), 14 | testCase(TaskTests.allTests), 15 | testCase(UnfairLockTests.allTests) 16 | ] 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ConcurrencyKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ArrayConcurrentMapTests.allTests() 7 | tests += AtomicBoolTests.allTests() 8 | tests += AtomicIntTests.allTests() 9 | tests += AtomicTests.allTests() 10 | tests += DispatchQueue_AsyncAfterTests.allTests() 11 | tests += DispatchQueue_OnceTests.allTests() 12 | tests += ReadWriteLockTests.allTests() 13 | tests += StatefullOperationTests.allTests() 14 | tests += TaskTests.allTests() 15 | tests += UnfairLockTests.allTests() 16 | XCTMain(tests) 17 | -------------------------------------------------------------------------------- /logo-concurrency_kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleev/concurrency-kit/ed10d294e66b8d714767090fa70608eb31a0289d/logo-concurrency_kit.png --------------------------------------------------------------------------------