Threads
async keyword specifies that function will be executed in background.
await keyword in front of the call allows to mark the possible suspension point.
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
// wait photos, current execution will be suspended
let photoNames = await listPhotos(inGallery: "Summer Vacation")
To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
Grand Central Dispatch (GCD)
Dispatch framework allows to execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system. Dispatch, also known as Grand Central Dispatch (GCD)
DispatchQueue is an object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.
DispatchQueue.main is predefined queue that is associated with the main thread of the current process.
DispatchQueue.global() allows to get global system queue with the specified quality-of-service (QoS)
let label = UILabel()
DispatchQueue.global(qos: .userInitiated).async {
let text = loadArticleText() // load text in background
DispatchQueue.main.async {
label.text = text // update ui in main thread
}
}
You can execute the code after the specified delay.
// execute after 1.5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
// your code here
}
// also .seconds(Int), .microseconds(Int) and .nanoseconds(Int) may be used
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
// Code
}
You can create own queue.
/* Serial queue with the default QoS, i.e.
any previous work has to be completed
before any new work will begin.
*/
let queueSerial = DispatchQueue(label: "CacheQueue")
/* Concurrent queue with a higher priority, i.e. multiple
works can be executed simultaneously.
*/
let queueConcurrent = DispatchQueue(
label: "ConcurrentQueue",
qos: .userInitiated, // specify a higher priority
attributes: [.concurrent] // specify queue must be concurent
)
QoS
A quality-of-service (QoS) class categorizes work to perform on a DispatchQueue. By specifying the quality of a task, you indicate its importance to your app.
There are predefined QoS in DispatchQoS structure.
QoS | description |
---|---|
userInteractive | The quality-of-service class for user-interactive tasks, such as animations, event handling, or updates to your app's user interface. |
userInitiated | The quality-of-service class for tasks that prevent the user from actively using your app. |
`default` | The default quality-of-service class. |
utility | The quality-of-service class for tasks that the user does not track actively. |
background | The quality-of-service class for maintenance or cleanup tasks that you create. |
unspecified | The absence of a quality-of-service class. |
operations
iOS has other API to perform tasks asynchronously:
- Operation - base class to represent task (NSOperation in objective C)
- InvocationOperation allows to create task from existing method using selector
- NSBlockOperation allows to create task from the specified block objects. The operation is considered finished when all blocks are executed.
- OperationQueue allows to regulate the execution of operations
This API uses GCD.
Unlike blocks in GCD, operations can be canceled, have some result and can depend from other operations.
Operations in queue can be suspend.
queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
// add block as operation
queue.addOperation {
// ... do something in background
OperationQueue.main.addOperation {
// ... do something in main thread
}
}
queue.cancelAllOperations()
// create block operations explicitly
let operation1 = BlockOperation {
// ...
}
let operation2 = BlockOperation {
}
operation2.addDependency(operation1)
operation2.completionBlock = {
// ...
}
You can create custom operation from Operation class.
class LoadOperation: Operation {
// will be called in background thread
override func main() {
// ...
}
}
By default, after the execution of the main() method, the completion block is executed.
But sometimes your task will be more difficult. For example, it may contain nested asynchronous code due to the use of some libraries such as Moya.
To control the execution of an operation, you must override the isExecuting and isFinished properties. In this case don't forget add KVO messages.
atomicity
NSLock object allows you to execute code atomically.
var lock = NSLock()
var a = [1, 2, 3]
lock.lock()
a.append(4)
lock.unlock()
Also you can add convenient extension for NSLock class.
extension NSLock {
@discardableResult
func withLock<T>(_ block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
/* usage
let lock = NSLock()
var a = [1, 2, 3]
lock.with { a.append(4) }
*/
test async
The xCode test API provides expectations which allow you to test asynchronous calls.
class MyClassTests: XCTestCase {
func testScalingProducesSameAmountOfImages() {
// create an expectation
let expectation = self.expectation(description: "async")
// call asynchronously something with completion handler
objTest.asyncCall(){
// do something ...
// set ok on completion handler
expectation.fulfill()
}
// wait for the expectation to be fullfilled, or time out
waitForExpectations(timeout: 5, handler: nil)
// test as usual
XCTAssertEqual(/*...*/, /*...*/)
}
}