Build a smooth iOS app

Build a smooth iOS app

In terms of building an iOS application, we want to make it smooth (aka, Responsive) (User can scroll up, scroll down, touch the buttons, swipe left, right, without lagging), It is important to understand the multithreading concept in iOS and how could we intelligently manage the way we call each operation( or function) to maximize the performance of CPU when it runs our app. (without crashing)

To archive that, we will learn about how Apple prepared the APIs to support Developers to build a Responsive app.

Managing tasks (or operations) is hard, and Apple handles it for us (Developer), and we just need to use it right.

Grand Central Dispatch

Prodivde high-level concurrency constructs

Works with Dispatch Queues

Manage ThreadPool to execute the tasks in the Queues

DispatchQueue

Is a queue, where the tasks to be temporary queue up, and wait for their turn to be executed.

FIFO, and Dispatch Queue can be Concurrent or Serial

When we put a task in a queue for a purpose, we need to decide, what type of Queue that suitable for it.

1. Concurrent Queue

Let’s say we added 10 tasks in the queue Because the queue is FIFO queue, so, the 1st task will be pop out to be executed, then, immediately the next one will be executed without waiting for the 1st one to finish.

If there is 10 available CPU’s thread allocated to your app, all the 10 tasks will be executed parallelly at the same time ( awesome 🙂 )

The example above is for the ideal senarios, you don’t have anything else running, so, all the CPU’s thread available just for your queue :).

Boom, 10 operations are just be run at the same time.

2. Serial Queue

Also FIFO, but, the 2nd task will need to wait for the 1st to finish. It means, all the tasks in the Serial Queue are guaranteed to be executed one by one, in FIFO order.

Even there are 10 threads available, sorry Sir, we don’t want to break the rule, you need to wait for the current task to finish first. ( said, GDC officer)

Executing tasks in a Dispatch Queue

Given the scenario that we call Dispatch Queue in a function, we are the Caller, who calls DispatchQueue to perform the task for us. There are 2 types of behaviors: Asynchronously/Synchronously perform the task.

If we don’t care about the task to finish, we can call DispatchQueue to perform the task Asynchronously

If we want to wait for the task to be executed and completed, we call DispatchQueue to perform the task Synchronously.

There is a controller ( or dispatcher) who will play the role of getting the task from the queue, and sending it to the Operator( or executor)

Given the scenario that we call Dispatch Queue in a function, this behavior table to memorize how the controller works in different ways.

Caller call DispatchQueue… Concurrent Queue Serial Queue
Asynchronously Non-blocking caller from calling the next function. ( doesn’t care about the task is done or not). And the tasks in the queue can be executed parallelly since CPU has multiple cores. Non-blocking caller from calling the next function. ( doesn’t care about the task is done or not). But the tasks in the queue will be line-up and executed one by one.
Synchronously Block caller from calling the next function, til the task finished. And the tasks in the queue can be executed parallelly since the CPU has multiple cores. Block caller from calling the next function, til the task is finished. But the tasks in the queue will be line-up and executed one by one.

There are some Queues are built-in, we can just use them, but remember, those queues are shared among the Operator System and Your Application in runtime.

  • DispatchQueue.global(): Shared Concurrent Queue

  • DispatchQueue.main(): Shared Serial Queue, and especially, this queue (and only this queue) is responsible for UI task, so if you update anything related to UI, you must to it in this main queue.

So, better we should create our own queues ( Custom Queue )

Some practical cases

Downloading a list of Images from a remote URL

  • Each task doesn’t need to wait for the previous one to finish, they can and should be executed parallelly. So seems like we don’t need a SerialQueue here, and also don’t need to wait for the Image to be downloaded before we call another function. I would choose to perform this task in a ConcurrentQueue and dispatch it Asynchronously.

Updating User Profile

  • Let’s say we want to update our name from Mr Nam to Mr Nam Nguyen, and return the new updated name. It seems like we will call a function profile.update(newName) and after that profile.getname(). We cannot use a Concurrent queue to update our name because when we call getname, we are not guaranteed that the name is updated or not. So better we wait for the task update(name) to finish. I would use SerialQueue and dispatch it synchronously.

Special case: Main Queue

Global Serial Queue was designed to contain only UI/Hardware related tasks. There is a special dedicated Thread that will only take the tasks from the Main Queue and execute it, called MainThread.

All the rest of Queues will be handled by a shared ThreadPool ( Grant Central Dispatch manages this thread pool).

So basically if you put too much work into this main queue( even for the tasks that do not require UI or Hardware related), the Main Thread will be busy, and the app will be lagging.

Important

Attempting to synchronously execute a work item on the main queue results in deadlock.

Apple

We need to avoid performing a task Synchronously using the main queue (DispatchQueue.main().sync({})) while we are working on the function that will be executed by main queue.

For example, we are in viewDidLoad, this is the function that will be executed in the main queue by default. And we do something like DispatchQueue.main().sync({ doingAThing(); }) inside it. And the main queue is a Serial Queue, so, every task added into it will be executed one by one.

  • viewDidLoad added in the queue.

  • doingAThing added in the queue.

As we mentioned before, MainQueue is First-In-First-Out, and there is only one Thread that will do the execution. So the deadlock happens :

  • Runloop tries to execute viewDidLoad, and it will not return till the function is finished.

  • Inside viewDidLoad, Runloop tries to execute doingAThing, since it is a Synchronously call, it will not return to the caller till the doingAThing() finishes.

  • The problem is, this is a SerialQueue, and doingAThing need to wait for viewDidLoad to finish, while viewDidLoad cannot finish because doing a thing() never returns.

  • That is why we called Deadlock. Just remember what Apple said, for Main Queue, don’t dispatch a task Synchronously if that task will be executed in the same queue.

That is about Queues, how about Threads

Ok, they are just Threads, no DispatchThread.

A Thread can be understood like some small program that can take the task (or operation) from a Queue, read it, break it into smaller operations (chunks), and put them into a Stack. A Thread will proceed the tasks from the Queue, one by one.

In iOS, GCD manages many Threads, including creating new ones, assigning the free one to take a task, or destroying unnecessary one. Among those threads, there is a very special Thread that is dedicated to taking the task from MainQueue, apparently, it is named MainThread.

NSRunloop (or Even Processing Loop) is a mechanism built-in every Thread, they will keep taking the Thread’s operation stack and run it. When there is no task in the Stack to pickup, it puts the Thread into sleep mode.

There is only one NSRunloop in each Thread.

The app that can leverage multiple Threads to execute the tasks parallelly, the more responsive it will be, so Developer must be good at this :).

In Runtime, all Threads share the same Virtual Memory

This means, your classes and their properties could be accessed ( Read, Write) concurrently from many Threads. So If you are not aware, the value of the properties could be wrong.

Thread-Safe Variable

For example, I have a class Student, with one property GPA, I Initialize an instance of Student

let aStudent = Student()
aStudent.gpa = 0.0
myClass.register(aStudent)
// do another thing, after awhile you go back to continue with your instance
...
aStudent.gpa += 2.0;
// we expect aStudent.gpa == 2.0, since it was initialized as 0, but wrong
// Another thread execution has been done parallelly on the gpa.

To avoid such cases like that, we want that our instance is synced between multiple Threads, we declare the property as atomic to make the compiler knows that, all operations on this property, need to proceed one by one across all the Threads.

In another hand, the attibute nonatomic means, all the Threads are freely to run on this property without worry about concurrency ( sounds scary huh 🙂 )

Thread-Safe APIs

Usually, if we use an API provided by Apple, If the document doesn’t say it is ThreadSafe, we need to treat it as Unsafe.

 

(…to be continue in Part II)

Leave a Comment

Your email address will not be published. Required fields are marked *