/concurrency-kit

πŸš„ Concurrency abstractions framework for Apple Platforms [Task, Atomic, Lock, Operation, etc.].

Primary LanguageSwiftMIT LicenseMIT

concurrency-kit Awesome

Build Coverage Platforms Language SPM License

Last Update: 05/January/2020.

If you like the project, please give it a star ⭐ It will show the creator your appreciation and help others to discover the repo.

✍️ About

πŸš„ Concurrency abstractions framework for iOS development [Task, Atomic, Lock, etc.].

πŸ”₯ Features

  • Atomics - synchronization primitive that is implemented in several forms: Generic, Int and Bool.
    • Fast. Under the hood a mutex (pthread_mutex_lock) that is more efficient than OSSpinLock and faster than NSLock.
    • Throwable. You can safely throw Errors and be able to delegate the handling.
  • Locks - contains a number of locks, such as:
    • 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.
    • ReadWriteLock - An RW lock allows concurrent access for read-only operations, while write operations require exclusive access.
    • Mutex - Allows only one thread to be active in a given region of code.
  • DispatchQueue+Extensions - extended DispatchQueue, where asyncAfter and once methods add convenience.
  • Task - A unit of work that performs a specific job and usually runs concurrently with other tasks.
    • Tasks can be grouped - meaning that you are able to compose the tasks, similar to Futures & Promises and execute them serially.
    • 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).
  • Stateful Operation - is a custom Operation class that supports modern, Swifty state management through the usage of Atomics and Enum types.
  • Thoroughly tested.

πŸ“š Examples

Task

In order to create a Task, you need to simply use the Task struct and the trailing closure syntax:

let uploadingTask = Task { controller in
  uploader(photos) { result in 
    switch result {
      case .success:
        controller.finish()
      case .failure(let error):          
        controller.fail(with error)
    }
  }
}
        
uploadingTask.perform { outcome in
  handle(outcome)
}

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:

let filesToUpload = [file, photo, video, xml]

let group = Task.group(fileToUpload)
group.perform { outcome in 
  handle(outcome)
}

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:

let filesToUpload = [file, photo, video, xml]

let group = Task.sequence(filesToUpload)
group.perform { outcome in 
  handle(outcome)
}

Stateful Operation

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:

class NetworkStatefulOperation: StatefullOperation {

  // MARK: - Properties
  
  private let callback: (StatefullOperation?) -> Void
  private let service: NetworkService             
  private let dataHandler: Parsable            
            
  // MARK: - Initializers
  
  init(_ service: NetworkService, _ dataHandler: Parser, callback: @escaping (StatefullOperation?) -> Void) {
    self.service = service
    self.dataHandler = dataHandler
    self.callback = callback
  }
  
  // MARK: - Overrides
  
  override func executableSection() {
    service.dataTask { [weak self] result in 
      self?.dataHandler.parse(result)
      self?.finishIfNotCancelled()
      self?.callback(self)
    }
  }
}

Then, the usage of the NetworkStatefulOperation class is quite straightforward:

// 1. Create an instance of `NetworkStatefulOperation` class:
let networkiOperation = NetworkStatefulOperation(service, parser) {
  // 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:
  updateUI(with: $0.state)
}
// 2. Then call the `start` method:
networkOperation.start()

Atomics

Guarantees safe mutation of a property in multiple async dispatch queues. Simply wrap a property in Atomic type:

let atomic = Atomic(0)

DispatchQueue.global().async {
  atomic.modify { $0 + 1 }
}

DispatchQueue.global().async {
  atomic.modify { $0 + 1 }
}

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)

Locks

Read Write Lock

A syncronozation primitive that solves one of the readers–writers problems:

let rwLock = ReadWriteLock()

rwLock.writeLock()
sharedValue += 1
rwLock.unlock()

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:

let rwLock = ReadWriteLock()

rwLock.readLock()
sharedValue += 1
rwLock.unlock()

Unfair Lock

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:

let unfairLock = UnfairLock()

unfairLock.lock()
sharedValue += 1
unfairLock.unlock()

Mutex

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:

let mutex = Mutex()

mutex.withCriticalScope {
  return sharedValue += 1
}

Dispatch Queue

There is a convenience method that removes the need to pass .now() + time in order to make an async call:

DispatchQueue.main.asyncAfter(seconds: 2.5) {
  expectation.fulfill()
}

Also, DispatchQueue.once was returned back::

// The following concurrentQueue is called multiple times, though the caughtValue will be set to value only once.
concurrentQueue.async {
  DispatchQueue.once(token: "caught") {
    caughtValue = value
  }
}

πŸ— Installation

Swift Package Manager

Xcode 11+

  1. Open MenuBar β†’ File β†’ Swift Packages β†’ Add Package Dependency...
  2. Paste the package repository url https://github.com/jVirus/concurrency-kit and hit Next.
  3. Select the installment rules.

After specifying which version do you want to install, the package will be downloaded and attached to your project.

Package.swift

If you already have a Package.swift or you are building your own package simply add a new dependency:

dependencies: [
    .package(url: "https://github.com/jVirus/concurrency-kit", from: "1.0.0")
]

Manual

You can always use copy-paste the sources method πŸ˜„. Or you can compile the framework and include it with your project.

πŸ‘¨β€πŸ’» Author

Astemir Eleev

πŸ”– Licence

The project is available under MIT licence