Running tasks in parallel is one of the great things we can do with computers – and mobile devices. This is called concurrency and it was the main focus for Week 7 of my iOS Development Bootcamp. In iOS, Apple provides three technologies to handle concurrency using Swift. These frameworks are designed to encapsulate units of work and dispatch them for execution. This week, we concentrated on two of them: “Grand Central Dispatch” (GCD) and “NSOperation frameworks”.
The personal challenge I had after learning these two approaches was when to pick “NSOperation frameworks” over “Grand Central Dispatch” (GCD) and vice versa. Because these APIs serve the same goal, hence the confusion about when to use which.
Below is a quick comparison of the two:
|Grand Central Dispatch||NSOperation|
|A lightweight way to represent units of work that are going to be executed concurrently. GCD uses closures to handle what runs on another thread.||Operations are objects that encapsulate data and functionality. Operations add a little extra development overhead compared to GCD.|
|The system takes care of scheduling for you. Adding dependencies, canceling, or suspending code blocks is labor intensive.||Allow for greater control over the submitted tasks, including some control over scheduling through adding dependencies among various operations and can re-use, cancel or suspend them.|
|GCD is good to use for simple, common tasks that need to be run only once and in the background.||Operations make it easier to do complex tasks. Use them when you need (a) task reusability, (b) considerable communication between tasks, or (c) to closely monitor task execution.|
Concurrency is running more than one task at the same time. Use concurrency to make the app more responsive.
Thread safety: A thread of execution is where a task runs. To run to tasks at the same time the app needs at least two threads. Threads are the lowest level of execution. They are what Xcode Debug Navigator shows you.
Grand Central Dispatch is an underlying lib dispatch library of the C programming language. Swift 3 updated the syntax to be closer to a normal Swift class.
It provides 5 global dispatch queues as well as the main queue, your app’s user interface. Queues are first in first out, where out means “start”. They start in the order they arrive, but tails running on a global queue might finish in a different order. This is because global queues are concurrent by creating more than one thread. The main queue is serial, i.e. it only executes one instruction at a time running in only one thread – the main thread.
Operations wrap object-oriented convenience around dispatch queues. Operations is a class that allows you to wrap up a unit of work or task into a package you can execute at some time one the future. We can set its properties any time up until we run it. We can specify inputs, output and helper methods, cancel a running operation or define dependencies on other operations. They are highly flexible and reusable. Most of the time we add operations to the an operations queue.
Block operations run concurrently. If we need to run them serially we need to submit them directly to a private dispatch queue.
GCD provides several dispatch queues, the main queue is used so often for user interface updates that it has a special name:
dispatchqueue.main and it is serial – the main thread. Other queues are concurrent and run with different priority or quality of service (qos). We can specify a concurrent global queue with the type method
dispatchqueue.global and a quality of service
qos enumeration value. From highest to low:
DispatchQueue.main()// .default qos
We mostly use levels 3-5
We can create a private serial queue if we want tasks to run exactly in the order they arrive. Private queues are serial by default. We use the
concurrent attribute to make them concurrent.
If a task is dispatched from the main queue the system infers it to be user initiated.
A major purpose of concurrency programming is to run non-user interface tasks off the main queue. The destination queue may be serial or concurrent.
Tasks can be dispatch synchronously or asynchronously from the current queue to another queue.
- If a task is dispatched async, the current queue can continue immediately with the next task.
- If we run a task sync form a serial queue, it myst wait until the task completes before it can do anything else.
The latter is useful if the next task used the same resource to make sure only one task at a time can change that resource. But if it is the main queue this may affect user experience as the interface will stop responding.
Sync v async is about the source of the task and tells us if the system needs to wait for the task to finish or not.
Serial v concurrent is about the destination of the task and it tells us whether the queue has one or more threads.
The quickest way to add tasks to a queue uses its async or sync method providing the task in a closure. Use the async method to move slow tasks off the main queue, and to update the user interface the main queue. Use the sync method to stop the current thread until a short but critical tasks completes on another queue.
In an app, always dispatch back to the main queue for user interface updates.
A task dispatched sync actually runs on the current thread, regardless of which queue you dispatch it to. This makes sense because the current thread has to wait for the task to return – its thread would not be doing anything – so may just as well do the task.
Reactive programming wraps APIs (timers, notification centres, delegates, operations, etc) into a single unified framework enabling us to declare what happens when data changes, using sequential logic that is easy to write, review and test. Reactive frameworks manage all the concurrency details to make this work. RxSwift was introduced in 2015, a year after Swift was released.
RxSwift and Combine
- Observables and observers
- Schedulers – SerialDispatchQueueScheduler, ConcurrectDispatchQueueScheduler, OperationQueueScheduler
- ULRSession.rx plus others
Apple created SwiftUI and Combine to work together, so it doesn’t make sense to use ordinary GCD or Operations in a SwiftUI only app. If you are integrating SwiftUI and UIKit, then you can save time by reusing GCD.
- Priority inversion: a lower priority task runs ahead of a higher priority task. Often because the high priority task needs a resource lower priority task has locked.
- Data races: Data races occur when multiple tasks or threads access a shared resource without sufficient protections, leading to undefined or unpredictable behavior.
- Deadlock: Two threads are each waiting for the other to release a shared resource or do something necessary for continuing
Jelly Belly – Update
This week there was no update to the Jelly Belly project. Instead, we had the pleasure of using the Playgrounds app (in iPad or Mac) to solve a number of challenges regarding concurrency.
Also published on Medium.