Supposing you have a mobile application running on the main thread that is responsible for execution of code that manipulate the UI. Whenever you are packing your app with time-consuming pieces of code like download from the web or image processing on the main thread, this significantly slows down the UI performance and might even lead to its complete freezing.
Multithreading in iOS
So is there a way to change the application architecture so that such issues never occur? I believe concurrency can help here as it is capable of executing two or more independent tasks, like calculations, data download from the web or a drive, image processing, etc.
This is not a free ride though, with the introduction of concurrency, code thread safety might get compromised in execution. As soon as you allow the tasks to be executed simultaneously, be prepared for issues with tasks gaining access to the same resources like changing the same variable on different threads, or accessing the resources already blocked by other tasks. This could ultimately lead to the demolition of those resources utilized on different threads.
In iOS development concurrency is used to improve productivity and UI responsiveness, provided by several tools like Thread, GCD (Grand Central Dispatch), and Operation. It’s safe to say that before Swift 3 emerged, the powerful GCD framework was using the C API which was a tangle of hidden opportunities for user actions. Swift 3 changed it all. The GCD got a a new easy-to-use syntax based on the GCD logic.
To better realize how to use concurrency, let’s figure out which key notions operate with both the GCD and Operation tools. The basic notion is the queue. So when talking about the iOS concurrency from a developer’s perspective, queues will most likely be mentioned. The queues have closures lined up and according to the specified order, the system pulls them one by one and deploys them on the appropriate threads. The queues follow the FIFO (first in, first out) principle in multiple threads constituting the concurrency pattern.
Serial Queues Vs Concurrent Queues
The queues can be:
- Serial or consecutive, when the closure at the top of the queue gets pulled by iOS and operates until it ends, then pulls another queue element and so on.
- Concurrent or multithreaded, when the system pulls a closure at the top of the queue and initiates its execution in a certain thread.
If the system has access to more resources, it picks the next element from the queue and launches it on a different thread while the first function is still at work. This way the system can pull a number of functions.
Synchronous Methods Vs Asynchronous Methods
As soon as the queue is created, there are two methods of placing jobs into it:
- Synchronous method present a synchronous tasking placement related to the current queue. The sync method returns the control to the current queue after the entire task has been completed and thus, blocks the current queue.
- Asynchronous method is an asynchronous tasking placement related to the current queue. Async as opposed to the sync method, returns the control to the current queue immediately after the launch of the task on a different queue without waiting for it to end. So the async method does not block the task execution on the current queue.
A different queue might occur as:
- A Serial queue in case of an asynchronous execution method or concurrent in case of synchronous execution.
- A Concurrent queue.
A developer’s task lies exclusively in the queue selection and adding tasks (closures) into that queue synchronously with the help of the sync method, or asynchronously, by means of the async method. iOS takes it from there.
Global Dispatch Queues
Besides the custom-made user queues, iOS offers some out-of-the-box global dispatch queues of five kinds:
- Main queue. Responsible for all the UI manipulations:
let main = DispatchQueue.main
If you need to execute a function or a closure, affecting the UI-anything, this function or closure has to be placed on the Main Queue, as this is the first priority global dispatch queue.
- Four background concurrent queues with different QoS capabilities and priorities:
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)
let userInitiatedQueue = DispatchQueue.global(qos: .userInitiated)
let utilityQueue = DispatchQueue.global(qos: .utility)
let backgroundQueue = DispatchQueue.global(.background)
let defaultQueue = DispatchQueue.global()
Each of these queues Apple has has equipped with an abstract QoS and it has to be configured specifically for each task. The following is the list of available QoSs with their brief function descriptions:
- .userInteractive is the quality for the tasks interacting with a user at the very moment and take little to no time. Animation execution is instant but it not on the Main Queue. For example, a user swipes the screen while the system performs image processing calculations on that queue. The result is slightly delayed from the swiping as the calculations require some time. This queue is of very high priority, though lover than of the main Queue.
- .userInitiated is for the tasks initiated by a user and requires feedback outside the interactive event. The feedback is required for a user to continue interaction which might take a few seconds, has high priority but lower than of the previous queue.
- .utility is for the tasks that require certain time to be executed and need no immediate feedback, for example data loading and database cleanup. Some system maintenance services are run with no user intention, which can take up to several minutes. The priority is lower than of the previous queue.
- .background is for the tasks unrelated to the visualization and irrelevant to the execution time. For example, backups or web service sync. These are usually started in the background when no maintenance is required. The background task can take up to hours and has the lowest priority among all the global dispatch queues.
There is also a global concurrency queue called .default which reports the absence of information about the QoS. This queue is created by means of the DispatchQueue.global() operator.
As soon as tasks are granted concurrency capabilities, they go to battle for available resources. There are three main problems associated with this:
- Race condition. Signifies a multithreaded system or application projection error that occurs when the system or application operation depends on how the arts of code are executed.
- Priority inversion. This problem occurs when there is a logical discrepancy with planning rules, as the higher-priority task is pending while the lower-priority task is executed based on the queue succession.
- Deadlock. This is the situation in a multithreaded system when several threads are in the state of infinite wait for resources, occupied by these very threads.
In iOS development, concurrency is used to enhance the app’s performance by dividing tasks to different threads capable of independent operation. Concurrency, however is a difficult condition that requires deep analysis of what is to be optimized in terms of tasks and queue priorities.