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

  1. GCD

  2. Operation queue

  3. 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 

By default GCD provides these built-in queues, to ease dispatching tasks

  • 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 

Gurudongmar Lake - Sikkim | Sikkim, Landscape photography, Lake


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

Apple reference on .barrier

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

DispatchWorkItem Reference

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.

DispatchGroup reference


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.

Semaphore Reference

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


Reference :

OperationQueue by avanderlee

Comments