Multithreading in iOS-Part 4/4

Manasa M P
17 min readJun 22, 2021

--

Before starting this blog please refer the blog to know more about

GCD and thread

DispatchWorkItem, DispatchGroup, DispatchBarrier, DispatchSemaphore, DispatchSources

NSOperation queue

How can we cancel task in GCD?

Sometimes, we need extra control over the execution. To be able to cancel a task in GCD, create a work item:

If task is not started, then this will be canceled else task can’t be cancelled instead it will set isCancelled value to true.

Why do we need to avoid excessive thread creation?

After we discuss about use of multithread developer might think to create a lot of queue to get better performance but thread creation comes with a cost as we discussed in part 1. so we should avoid excessive thread creation.

2 scenario where excessive thread creation occurs

  1. Too many blocking task are added to concurrent queues forcing the system to create additional threads until system run out of threads for your app.
  2. Too many private concurrent dispatch queues exist that all consumes thread resource.

How can we prevent this? Best practice is to use the global concurrent dispatch queues.

What happens when you block main thread?

The application is performing badly because we’re blocking the main thread.

The user interface of the application is drawn to the screen on the main thread. Drawing the user interface to the screen isn’t a cheap operation. It takes time and resources. For smooth scrolling, the application needs to have the resources to draw the user interface to the screen several times per second. That isn’t possible if the main thread is blocked.

How can we prevent this? Use multithread advantage by Grand Central Dispatch or NSOperation and use async(group:qos:flags:execute:) call on queue to prevent main thread from being blocked

If you use sync(group:qos:flags:execute:) again it will block the main thread

What is DispatchQueue.main and DispatchQueue.global ?

DispatchQueue.main: The main thread, or the UI thread, is a single serial queue. All tasks are executed one by one, so it is guaranteed that the order of execution is preserved

DispatchQueue.global: A set of global concurrent queues, each of which manage their own pool of threads. Depending on the priority of your task, you can specify which specific queue to execute your task. It doesn't guarantee preservation of the order in which tasks were queued.

Why we need to update UI in main thread?

Most of the components in UIKit is described as nonatomic`, this means there are not thread safe and it is difficult to design all the properties as thread-safe in UIKit because it is very huge framework. Each thread has its own run loop.

Bellow are few example questions/problem we might face when we update UI on background thread.

  • Assume you can change view’s properties asynchronously, shall these changes become effective at the same time?
  • If a UITableView remove a cell on a background thread, then another background thread operate this cell’s index, it may causes crash
  • If a background thread remove a view, and this thread’s RunLoop is not over, at the same time the user tap this removed view then what happens should it respond to the touch event? which thread need to respond?

To avoid all these kind of issue, it’s better to use main thread to update UI related task.

We might again think like:- If i use wrapper class on top of the UIKit and to these above stuff in a Serial queue, then what happens? then Can we update UI on background thread? No we can’t

This is because of View rendering happens at the end of run loop and each thread has its own run loop. If we update UI on background thread then no guarantee that when that runloop update the UI and our UIKit is not on main thread, so the user events in Main RunLoop is not synchronised with display.

What happen if you refactor whole UIApplication user event mechanism, and now it can handle the problem of thread synchronous, then can we update UI on background threads? — No we can’t

If you want to know answer for this first we need to understand iOS rendering process.

  • UIKit: Contains all kinds of components, handles user events, it does not contain any rendering code.
  • Rendering Server: Responsible for drawing and displaying the view. which is a part in core animation.
  • Core Animation: Responsible for drawing, displaying and animating all views.

in iOS, all views are display and animate by Core Animation Framework, not UIKit.

Core Animation use Core Animation Pipeline to rendering, which is divided into four steps.

  • Commit Transaction: It handles layout of views, image decoding and format conversion operations and send to Render Server.
  • Render Server: Rendering, analyse the package sent from Commit Transaction and deserialisation into a rendering tree. Then it will generate drawing instructions by view layer’s properties, and call OpenGL to render screen when the next VSync Signal comes.

Vertical Sync (VSYNC): This signal is transmitted after the entire frame is transferred. This signal is often a way to indicate that one entire frame is transmitted.

  • OpenGL ES: Provide 2D and 3D rendering server.
  • Core Graphics: Provide 2D rendering server.
  • GPU: GPU will wait for screen’s VSync Signal, then use OpenGL rendering pipeline to render. After rendering the output will send to buffer i.e frame buffer.
  • Display: Get data from buffer, and send to screen to display.

If we use our own wrapper class around UIKit then we might end up with rendering problem because each thread commits different render information, so we have to handle more Commit Transactions and the Core Animation Pipeline will commit informations to GPU all the time. Rendering is actually a very expensive operation of the system resources and frequent context switching between threads and a large number of transactions causes the GPU to be un-processable, which in turn affects performance

If you really want to update UI on background thread then you can relay on some third party frame work like Texture and ComponentKit developed by Facebook.

These two framework are not actually update UI on background threads, instead it use some time-consuming operations asynchronously, bypassing the limitation that UI can only be updated on the main thread.

Why UIKit is not thread safe?

If apple make UIKit thread-safe and it make many things slower. It’s very easy to write concurrent programs and use UIKit. One thing you need to do is that calls into UIKit are always made on the main thread.

What happens when we Dispatching on the same queue?

In above example, for the concurrent queue a new thread is brought up in the pool to service each async operation. The pool hits the limit at number of threads on your machine(consider limit is 65 threads).

From the main queue we call a sync operation on the same queue. The main thread is blocked, since there are no available threads in the pool. At the same time, all the threads in the pool are waiting for the main thread. Since they are both waiting for each other, hence the deadlock occurs.

it is safe to dispatch a task asynchronously from a queue into itself. you can not dispatch a task synchronously from a queue into the same queue. Doing so will result in a deadlock that immediately crashes the app!

Deadlock is a situation where a set of processes are blocked because each process is holding a resource and waiting for another resource acquired by some other process.

When Deadlock occurs?

A situation where a thread locks a critical portion of the code and can halt the application’s run loop entirely. In the context of GCD, you should be very careful when using the dispatchQueue.sync { } calls as you could easily get yourself in situations where two synchronous operations can get stuck waiting for each other. This causes deadlock.

How can we prevent this? Use async to avoid such crashes.

How can you limit a queue to perform limited concurrent tasks?

Use semaphore to limit a queue to perform limited concurrent tasks:

What is Producer-Consumer Problem or race condition?

Swift Arrays, Dictionaries, Structs, and other value types are not thread-safe by default. For example, when you have multiple threads trying to access and modify the same array, you will start running into trouble.

Let’s say we have an array of integers, and we want to submit asynchronous work that references this array. As long as our work only reads the array and does not modify it, we are safe. But as soon as we try to modify the array in one of our asynchronous tasks, we will introduce instability in our app. This is called race condition or producer-consumer problem

A race condition occurs when one thread is creating a data resource or-reading data while another thread is accessing it or writing on it. This is a synchronisation problem.

How can we prevent this? can be solved using locks, semaphores, serial queues, or a barrier dispatch if you’re using concurrent queues in GCD.

Lock is an abstract concept for threads synchronisation. The main idea is to protect access to a given region of code at a time. Different kinds of locks exist:

  1. Semaphore(DispatchSemaphore) — allows up to N threads to access a given region of code at a time.
  2. Mutex — ensures that only one thread is active in a given region of code at a time. You can think of it as a semaphore with a maximum count of 1 or pthread_mutex_t.
  3. Spinlock(OSSpinLock) — causes a thread trying to acquire a lock to wait in a loop while checking if the lock is available. It is efficient if waiting is rare, but wasteful if waiting is common.
  4. Read-write lock (pthread_rwlock_t)— provides concurrent access for read-only operations, but exclusive access for write operations. Efficient when reading is common and writing is rare.
  5. Recursive lock — a mutex that can be acquired by the same thread many times.

GCD Barriers:When the barrier task enqueued, tasks in the queue are split in 3 parts: tasks enqueued before, after, and the barrier task itself. Tasks enqueued before the barrier executed as scheduled, concurrently. The barrier task is executed after, exclusively. And queue continues executing concurrently.

Priority Inversion:

A condition where a lower priority task blocks a high priority task from executing, which effectively inverts their priorities. GCD allows for different levels of priority on its background queues.

This situation often occurs when a high QoS queue shares a resources with a low QoS queue, and the low QoS queue gets a lock on that resource.

when you submit tasks to a low QoS serial queue, then submit a high QoS task to that same queue. This scenario also results in priority inversion, because the high QoS task has to wait on the lower QoS tasks to finish.

As you observe the above code utility qos is highest one but it finishes last because we have backgroundQueue.sync {}

as expected, GCD resolves this inversion by raising the QoS of the entire queue to temporarily match the high QoS task. Consequently, all the tasks on the background queue end up running at user interactive QoS, which is higher than the utility QoS. And that’s why the utility tasks finish last!

Run Loop and View Drawing Cycle

RunLoop is the basic function related to threads, it is used to schedule work and coordinate the event processing loop that receives incoming events. The goal of runloop is to let the thread work when there is a task, and sleep when there is no task processing.

What the purpose of runloop?

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. It handles sources of input and also generate notifications about the run loop’s behaviour. Registered run-loop observers can receive these notifications and use them to do additional processing on the thread.

Every thread has a single run loop object associated with it. In Cocoa, this object is an instance of the NSRunLoop class. In a low-level application, it is a pointer to a CFRunLoopRef.

How can you get a Run Loop Object?

To get the run loop for the current thread, you use one of the following:

  • In a Cocoa application, use the currentRunLoop class method of NSRunLoop to retrieve an NSRunLoop object.
  • Use the CFRunLoopGetCurrent function
1. get current runloop and 2. Get main runloop

How can you stop the runloop?

There are several ways to start the run loop, including Unconditionally, With a set time limit, In a particular mode

There are two ways to make a run loop exit before it has processed an event:

  1. Configure the run loop to run with a timeout value. :- Specify a timeout value lets the run loop finish all of its normal processing, including delivering notifications to run loop observers, before exiting.
  2. Tell the run loop to stop:- stopping explicitly using CFRunLoopStop function. This also produce same result as timeout. The run loop sends out any remaining run-loop notifications and then exits. The difference is that you can use this technique on run loops that you started unconditionally.

Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop.

This is because consider example that some system routines add input sources to a run loop to handle events, during this time your run loop will not exit because your code might not be aware of these input source.

When would you use a Run Loop?

Application’s main thread run loop is crucial so app framework provide the code for running the main application loop and start that loop automatically.

The run method of UIApplication in iOS (or NSApplication in OS X) starts an application’s main loop as part of the normal startup sequence. These methods will run the main threads runloop automatically.

When you create a secondary threads for your app then you need to run a run loop explicitly. You need to decide whether a run loop is necessary or not. If it is necessary then you need to configure and need to start yourself.

Note: You do not need to start a thread’s run loop in all cases. For example, you can probably avoid starting run loop when you use a thread to perform some long-running and predetermined task.

It’s very much required when you want more interactivity with the thread like you need ports or custom input sources to communicate with other threads or you need timer to perform some periodic task.

What is runloop Mode?

Runloop modes limit the types of sources that deliver events to the run loop. It’s discriminated based on the source of the event, not the type of the event. For example, you could not modes to match only mouse-down events or only keyboard events or listen to a different set of ports or suspend timers temporarily.

It is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time when you run runloop, you need to specify explicitly or implicitly the mode in which it has to run.

During this sources associated with runloop mode are monitored and allowed to deliver their events. Sources associated with other modes hold on to any new events until subsequent passes through the loop in the appropriate mode.

Registered run-loop observers can receive these notifications and use them to do additional processing on the thread.

There a few predefined runloop modes are there like

  • Default:- Most of the time, you should use this mode to start your run loop and configure your input sources.
  • Connection:- Cocoa uses this mode in conjunction with NSConnection objects to monitor replies.
  • Modal:- Cocoa uses this mode to identify events intended for modal panels.
  • Event tracking:- uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops.
  • Common modes:- This is a configurable group of commonly used modes. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using the CFRunLoopAddCommonMode function.

You can define custom modes by specifying a custom string for the mode name.

You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create to use them.

How runloop receives events?

A run loop receives events from two different types of sources.

  1. Input sources deliver:- It is a asynchronous events, usually messages from another thread or from a different application or mouse click or any user interaction
  2. Timer sources deliver:- Synchronous events, occurring at a scheduled time or repeating interval. Both types of source use an application-specific handler routine to process the event when it arrives.

Input Source:-

The source of the event depends on the type of the input source which delivers the event asynchronously to your threads. Which is generally one of these categories.

  1. Port-Based Sources
  2. Custom Input Sources

Port-Based Sources:-

Cocoa and Core Foundation provide built-in support for creating port-based input sources using port-related objects and functions.

For example, In Cocoa, no need to create an input source directly. Just create a port object by using the methods of NSPort to add that port to the run loop. The port object handles the creation and configuration of the needed input source.

In Core Foundation, Manually you need to create both the port and its run loop source. In both cases, you use the functions associated with the port to create object

Custom Input Sources:-

Using CFRunLoopSourceRef in Core Foundation, you can create custom input source and can be configured using several callback functions. Need to define behaviour of the custom source when an event arrives and event delivery mechanism. This part of the source runs on a separate thread and is responsible for providing the input source with its data and for signalling it when that data is ready for processing.

When you create an input source, you assign it to one or more modes of your run loop. Modes affect which input sources are monitored at any given moment. Most of the time, you run the run loop in the default mode, but you can specify custom modes too. If an input source is not in the currently monitored mode, any events it generates are held until the run loop runs in the correct mode.

For runloop, it should not matter whether an input source is port-based or custom. The only difference between the two sources is how they are signalled. Port-based sources are signalled automatically by the kernel, and custom sources must be signalled manually from another thread.

In addition to port-based sources, Cocoa defines a custom input source that allows you to perform a selector on any thread. Like a port-based source, perform selector requests are serialised on the target thread. Perform selector source removes itself from the run loop after it performs its selector.

Timer Sources:-

Timer sources deliver events synchronously to threads at a preset time in the future. Timer sources deliver events to their handler routines but do not cause the run loop to exit. For example, a search field could use a timer to initiate an automatic search once a certain amount of time has passed between successive key strokes from the user.

Although it generates time-based notifications, a timer is not a real-time mechanism. Like input sources, timers are associated with specific modes of your run loop. If a timer is not in the mode currently being monitored by the run loop, it does not fire until you run the run loop in one of the timer’s supported modes. Similarly, if a timer fires when the run loop is in the middle of executing a handler routine, the timer waits until the next time through the run loop to invoke its handler routine. If the run loop is not running at all, the timer never fires.

You can configure timers to generate events only once or repeatedly. A repeating timer reschedules itself automatically based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so much that it misses one or more of the scheduled firing times, the timer is fired only once for the missed time period. After firing for the missed period, the timer is rescheduled for the next scheduled firing time.

create timer in current run loop
create a runloop within the thread

It’s not thread-safe. Don’t call the methods of a NSRunLoop object running in a different thread, which might cause unexpected results.

Run Loop Observers:-

You might use run loop observers to prepare your thread to process a given event or to prepare the thread before it goes to sleep. You can associate run loop observers with the following events in your run loop:

  • The entrance to the run loop.
  • When the run loop is about to process a timer.
  • When the run loop is about to process an input source.
  • When the run loop is about to go to sleep.
  • When the run loop has woken up, but before it has processed the event that woke it up.
  • The exit from the run loop.

You can add run loop observers to apps using Core Foundation. To create a run loop observer, create a new instance of the CFRunLoopObserverRef. This type keeps track of your custom callback function and the activities in which it is interested.

The Run Loop Sequence of Events:-

Each time you run runloop, your thread’s run loop processes pending events and generates notifications for any attached observers. The order in which it does this is very specific and is as follows:

  1. Notify observers that the run loop has been entered.
  2. Notify observers that any ready timers are about to fire.
  3. Notify observers that any input sources that are not port based are about to fire.
  4. Fire any non-port-based input sources that are ready to fire.
  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
  6. Notify observers that the thread is about to sleep.
  7. Put the thread to sleep until one of the following events occurs:
  • An event arrives for a port-based input source.
  • A timer fires.
  • The timeout value set for the run loop expires.
  • The run loop is explicitly woken up.

8. Notify observers that the thread just woke up.

9. Process the pending event.

  • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
  • If an input source fired, deliver the event.
  • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.

10. Notify observers that the run loop has exited.

Conclusion:-

As we known, UIApplication will init a RunLoop on main thread, which calls Main RunLoop , it will handle most user event during the application life time such as user interactive and so on. It has been in the loop of constantly processing events and hibernation to ensure that user events can be responded as soon as possible. The reason why the screen can be refreshed is because `Main Runloop` is driving.

Also every view’s changes will not change immediately, they will redraw at the end of current RunLoop. This ensure the application can handle all the changes for all the view, and all the changes can become active at the same time. This is called View Drawing Cycle.

Reference:- https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

Enjoy your coding. I hope you learnt something from this blog. Please hit the clap button below 👏 to help others find it!. follow me on Medium.

--

--