Gateway to Multithreading in iOS with GCD and OperationQueue
This article revolves around api’s available in swift for achieving concurrency and advanced thread handling techniques.
Before we jump-in, it is better to brush-up few basics understandings of concurrent programming
lets start with concurrent vs parallel running of jobs...
Concurrency vs Parallelism
Concurrency means executing multiple tasks at the same time but not necessarily simultaneously.
Parallelism is when multiple tasks OR several part of a unique task literally run at the same time, e.g. on a multi-core processor.
Parallelism must require hardware with multiple processing units. In single core CPU, you may get concurrency but NOT parallelism.
more on concurrency and parallelism
Task / work item / block
Piece of code/task which needs to executed
Process
A process is the instance of a computer program that is being executed by one or many threads
Processor
The hardware within a computer that executes a program
Threads
A thread is a sequence of instructions that can be executed by a runtime. Each process has at least one thread. In iOS, the primary thread on which the process is started is commonly referred to as the main thread. This is the thread in which all UI elements are created and managed.
Priority
Value indicating which tasks needs to be prioritised while providing resource for execution, in GCD this is generally specified using 6 Quality of Service.
Tasks can be synchronous or non synchronous.
Synchronous
a.k.a blocking; simply means tasks are dependent/wait on other tasks to complete before proceeding further.
Non synchronous
a.k.a non-blocking; means task are independent and can run independently without waiting for other tasks to complete .
Queues
Regulates the execution of operations, FIFO structure for handling tasks which needs to be executed.
Queue can be serial or concurrent
Serial
When we say Queue is serial, tasks follow FIFO in completion one after the other.
Concurrent
When we say Queue is concurrent / non-serial, tasks follow FIFO in starting the execution but doesn't guarantee completion in FIFO order
Thread pool
Collection of threads, GCD has 4 global threads in thread pool by default.
Run loop
A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
Race condition
Situation where multiple threads try to access/change the same data/memory at the same time.
Deadlock
A deadlock occurs when two threads get stuck waiting for each other to finish. For example, Thread A and Thread B are requesting each others resources, and are dependent on each other's completion so neither thread will be able to continue.
Reader / Writers problem
Situation where an object such as a file that is shared gets updated from different threads and may cause inconsistency in the data when queried or updated.... To solve this situation, a writer should get exclusive access to an object i.e. when a writer is accessing the object, no reader or writer may access it. This can solved by .barrier or .lock in iOS
Frameworks / api's for thread handling in iOS
GCD
Operation queue
Semaphores
GCD
Low level c based api for concurrency handling in iOS.
Hence GCD is much faster compared to OperationQueue in how tasks are handled across threads.
Assuming we have got enough understanding of synchronous and asynchronous jobs, we will explore multiple situations where we try to understand the behaviour of the API
While executing on serial queue (say Q1)
1. Calling sync task on the same queue(Q1) causes deadlock and will crash.
2. Calling async task on the same queue(Q1) adds tasks to end of queue for execution.
While executing on serial/concurrent queue(say Q1) and dispatching tasks onto serial queue(say Q2)
1. Calling sync task on Q2 ensures whatever job Q2 is given is done first and then rest continues in Q1
2. Calling async task on Q2 ensures execution on Q1 continues(non blocking) , Q2 serially does all tasks dispatched as and when it gets free , Q2 tasks are serialised
While executing on serial/concurrent queue(say Q1) and dispatching tasks onto concurrent queue(say Q2)
1. Calling sync tasks on Q2, blocks Q1 further execution until submitted task on Q2 completes and return control to Q1.
2. Calling async tasks on Q2; both tasks on Q1 and Q2 continues, Q2 executes the submitted task concurrently
play with the following code to understand more..
Default/Built-In queues with GCD
- 1-serial main queue
- 4 global concurrent Queue with (Hight, Default, Low, Background) priority
Custom made Queues with GCD
Although there are built-in queues for help, we can create custom configured queues based on need. All custom queues created are serial in nature by default. Behaviour of these queues depend on the QOS configured while creating, serial/concurrent and many other customisations done.
Quality of services
By assigning a QoS to work, you indicate its importance, so the system prioritizes it and schedules it accordingly.
userInteractive ---- highest priority
userInitiated
Default
utility
background
Unspecified ----- lowest priority
more on QOS
Time to relax
Deadlock
As we are already aware of this situation from our basic section in the post, will discuss one such situation of deadlock while dealing with GCD.
Submitting an synchronous task from and to the same/currently executing serial queue will cause deadlock, so it crash on calling DispatchQueue.main.sync in viewDidLoad.
Race condition
A data race can occur when multiple threads access the same memory without synchronisation and at least one access is a write. You could be reading values from an array from the main thread while a background thread is adding new values to that same array.
Preventing race condition
1. In concurrent queues using .sync(.barrier)
.barrier will momentarily make concurrent queue behave in a serial way
By inserting a barrier to the write operation, we ensure that the writing will occur after all the reading in the queue is performed and that no reading occurs while writing. Example : reading messages from shared data collection in chat application
2. We can also use .lock , .unlock to make sure only thread is accessing the shared resource at any point in time.
Dispatch work item
Just another block of code
After you define dispatchWorkItem you can add following actions on it
.perform
.notify
.cancel
Also access item properties like
.isCancelled
Dispatch Group
The dispatch group allow to track the completion of different work items, even if they run on different queues.
Use it when you need to get notified of completion of independently running tasks across different threads or queues.
Example: when you have an UI which should be loaded once you receive response from 3 dependent api's.
Semaphore
Contrast to Dispatch Group, use semaphore when you want to. restrict access to shared resources
DispatchSemaphore init function has one parameter called “value”. This is the counter value which represents the amount of threads we want to allow access to a shared resource at a given moment.
Semaphores should be setup within the same queue , we can add .wait with timeout
Example: when you need to download 100 songs in. playlist, but should only allow max 3 parallel downloads at any time.
OperationQueue
operationQueue is an abstraction over GCD, so use operationQueue only when you can achieve something easily here like dependency, priority, monitoring progress etc which otherwise could take some effort in GCD
All operations which can be easily done on OperationQueue can be done by GCD as well by using either of DispatchGroup / Semaphores etc. OperationQueue just provides the ease of use for more complicated use cases.
Advantages
add dependency between tasks
monitor tasks: resume, cancel etc
Observe progress of task
add priority to task
Comments
Post a Comment