What is Multithreading? Complete Guide.

Main thread vs. background thread. Async/await and Actor. GCD vs. OperationQueue. Group dispatch, how to empower background thread, and more

Varga Zolt
Multiple threads through a needle
Photo by John Anvik on Unsplash

In this article, we will learn the following:

TABLE OF CONTENTSWhat Is Multithreading
Serial Queue vs. Concurrent Queue
Basics of Multithreading
Main Thread (UI Thread) vs. Background Thread (Global Thread)
GCD (Grand Central Dispatch)
Dispatch Barrier
(NS)Operation and (NS)OperationQueue
DispatchSource (How To Handle Files and Folders)
Deadlock (Issue What To Avoid)
Main Thread Checker (How To Detect Thread Issues)
Threads in Xcode (How To Debug Threads)
Async / Await / Actor Is iOS13+

I know there are lots of topics. If something is already familiar to you, skip it and read the unknown parts. There are tricks and tips.

Imagine you have a restaurant. The waiter is gathering the orders. The kitchen is preparing the food, the bartender is making the coffee and the cocktails.

At some point, lots of people will order coffee and food. That needs more time to get prepared. Suddenly the bell rings that 5x food is ready and 4x coffees. The waiter will need to serve the tables one by one even if all the products are prepared. This is the Serial Queue. With a serial queue, you are limited to only one waiter.

Now imagine there are two or three waiters. They can serve the tables much faster at the same time. This is Parallelism. Using multiple CPUs for operating multiple processes.

Now imagine one waiter will not serve one table at once, but will first serve all the coffees to all tables. Then, they will ask for the orders for some of the new tables, then serve all the food. This concept is called Concurrency. It is context switching, managing, and running many computations at the same time. It doesn’t necessarily mean they’ll ever both be running at the same instant. For example, multitasking on a single-core machine.

Today’s devices all have multiple CPUs (Central Processing Unit). To be able to create apps with seamless flows, we need to understand the concept of Multithreading. It is a way how we will treat some tasks in the application. It is important to understand that if something “works,” maybe it is not the best, desired way. Lots of times I see a long-running task that is happening on the UI thread and blocking the execution of the application for a couple of seconds. Nowadays it could be the no-go moment. Users could delete your application as they feel that other apps start up faster, make faster fetch of the books, music. Competition is big, and high standards are expected.

Thread execution types

You can see the “Serial 6” task if it is scheduled at the current time. It will be added to the list in the FIFO manner and waiting to be executed in the future.

One thing I have struggled with is that there is no standard terminology. To help with that for these subjects I will first write down the synonyms, examples. If you come from some other technology than iOS, you could still understand the concept and transfer it as the basics are the same. I had luck that early in my career I was working with C, C++, C#, node.js, Java(Android), etc. so I got used to this context switching.

  • Main thread / UI Thread: This is the thread that is started with the application, pre-defined serial thread. It listens for user interaction and UI changes. All the changes immediately need a response. Need to care not to add a huge job to this thread as the app can freeze.
Long-running task on UI Thread (wrong and should not do this)
DispatchQueue.main.async {
// Run async code on the Main/UI Thread. E.g.: Refresh TableView
  • Background Thread (global): Predefined. Mostly we create tasks on new threads based on our needs. For example, if we need to download some image that is big. This is done on the background thread. Or any API call. We don’t want to block users from waiting for this task to be finished. We will call an API call to fetch a list of movies data on the background thread. When it arrives and parsing is done then we switch and update the UI on the main thread.
Long-running task (done the right way on Background Thread)
DispatchQueue.global(qos: .background).async {
// Run async on the Background Thread. E.g.: Some API calls.
Example of SerialQueue

In the picture above, we added a breakpoint to line 56. When it is hit and the application stops, we can see this on the panel on the left side of the threads.

  1. You can see the name of the DispatchQueue(label: “com.kraken.serial”). The label is the identifier.
  2. These buttons can be useful to turn off / filter out the system method calls to see just user-initiated ones.
  3. You can see that we have added sleep(1). This stops the execution of the code for 1 second.
  4. And if you watch the order it is still triggered in a serial manner.

Based on the previous iOS, one of the two most used terms are the Serial Queue and Concurrent Queue.

Example of ConcurrentQueue
  1. This is result one of Concurrent Queue. You can see above the Serial / Main Thread also (com.apple.main-thread).
  2. The sleep(2) is added to this point.
  3. You see there is no order. It was finished async on the background thread.
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let serialQueue = DispatchQueue(label: “com.kraken.serial”)
let concurQueue = DispatchQueue(label: “com.kraken.concurrent”, attributes: .concurrent)

We can also create a private queue that could be serial and concurrent also.

GCD is Apple’s low-level threading interface for supporting concurrent code execution on multicore hardware. In a simple manner, GCD enables your phone to download a video in the background while keeping the user interface responsive.

“DispatchQueue is an object that manages the execution of tasks serially or concurrently on your app’s main thread or on a background thread.” — Apple Developer

If you noticed in the code example above, you can see “qos.” This means Quality of Service. With this parameter, we can define the priority as follows:

  • background — we can use this when a task is not time-sensitive or when the user can do some other interaction while this is happening. Like pre-fetting some images, loading, or processing some data in this background. This work takes significant time, seconds, minutes, and hours.
  • utility — long-running task. Some process what the user can see. For example, downloading some maps with indicators. When a task takes a couple of seconds and eventually a couple of minutes.
  • userInitiated — when the user starts some task from UI and waits for the result to continue interacting with the app. This task takes a couple of seconds or an instant.
  • userInteractive — when a user needs some task to be finished immediately to be able to proceed to the next interaction with the app. Instant task.

It’s useful is also to label the DispatchQueue. This could help us to identify the thread when we need it.

Often we need to start multiple async processes, but we need just one event when all are finished. This can be achieved by DispatchGroup.

“A group of tasks that you monitor as a single unit.”— Apple Docs

For example, sometimes you need to make multiple API calls on the background thread. Before the app is ready for user interaction or to update the UI on the main thread. Here’s some code:

Multithreading with DispatchGroup
  • Step 1. Create DispatchGroup
  • Step 2. Then for that group need to call group.enter() event for every task started
  • Step 3. For every group.enter() needs to be called also the group.leave() when the task is finished.
  • Step 4. When all the enter-leave pairs are finished then group.notify is called. If you notice it is done on the background thread. You can configure per your need.
Dispatch Group. Task one by one and all by notify.

It’s worth mentioning the wait(timeout:) option. It will wait some time for the task to finish but after timeout, it will continue.

“An object that controls access to a resource across multiple execution contexts through use of a traditional counting semaphore.” — Apple Docs

Multithreading with DispatchSemaphore

Call wait() every time accessing some shared resource.

Call signal() when we are ready to release the shared resource.

The value in DispatchSemaphore indicates the number of concurrent tasks.

A common belief is that when a GCD task is scheduled it can’t be cancelled. But this is not true. It was true before iOS8.

“The work you want to perform, encapsulated in a way that lets you attach a completion handle or execution dependencies.” — Apple Docs

For example, if you are using a search bar. Every letter typing calls an API call to ask from the server-side for a list of movies. So, imagine if you are typing “Batman.” “B,” “Ba,” “Bat”… every letter will trigger a network call. We don’t want this. We can simply cancel the previous call if, for example, another letter is typed within that one-second range. If time passes the one second and the user does not type a new letter, then we consider that API call needs to be executed.

SearchBar. Simulation “Debounce” with DispatchWorkItem

Of course, using Functional Programming like RxSwift / Combine we have better options like debounce(for:scheduler:options:).

Dispatch Barriers is resolving the problem with a read/write lock. This makes sure that only this DispatchWorkItem will be executed.

“This makes thread-unsafe objects thread-safe.” — Apple Docs

Dispatch Barrier
Barrier Timeline

For example, if we want to save the game, we want to write to some opened shared file, resource.

We can use this code to delay some task execution:

Evil AsyncAfter. Bad side of Multithreading

From my perspective, this is the source of all evil, with respect for exceptions. For every async task that needs a delay, I would suggest thinking through this, and if it is possible to use some state management system. Don’t pick this option as the first choice. Usually, there is another way.

If you are using NSOperation that means you are using GCD below the surface, as NSOperation is built on top of GCD. Some NSOperation benefits are that it has a more user-friendly interface for Dependencies(executes a task in a specific order), it is Observable (KVO to observe properties), has Pause, Cancel, Resume, and Control (you can specify the number of tasks in a queue).

OperationQueue Example. Option 2 for iOS Multithreading

You can set the concurrent operation count to 1 so it will work as a serial queue.

queue.maxConcurrentOperationCount = 1
Serial OperationQueue
Concurrent OperationQueue
Group Concurrent OperationQueue

This last is the Dispatch Group. The only difference is that it is much easier to write complex tasks.

DispatchSource is used for detecting changes in files and folders. It has many variations depending on our needs. I will just show one example below:

Example how to monitor some “physical” file

There is a situation when two tasks can wait for each other to finish. This is called Deadlock. The task will never be executed and will block the app.

Deadlock in iOS Multithreading

Never call sync tasks on the main queue; it will cause deadlock.

There is a way to get a warning that we did something wrong. This is a really useful option, and I recommend using it. It can easily catch some unwanted issues.

If you open on the target and edit the scheme as on the next image, turn on the Main Thread Checker, then when we do some UI update on background, this option on runtime will notify us. See the image below for the purple notification:

Main Thread Checker
Main Thread Checker result
Method name where is the issue can be seen

You can also see in the Xcode terminal what is wrong. For newcomers, it is maybe a bit of a strange message, but fast you will get used to it. But you can connect that inside that line there is the name of the method where the issue is.

While debugging, there are a couple of tricks that can help us.

If you add a breakpoint and stop at some line. In the Xcode terminal you can type the command thread info. It will print out some details of the current thread.

Debug the Threads in Code Terminal

Here are some more useful commands for the terminal:

po Thread.isMainThread

po Thread.isMultiThreaded()

po Thread.current

po Thread.main

Maybe you had a similar situation — when the app crashed and in the error log you could see something like com.alamofire.error.serialization.response. This means the framework created some custom thread and this is the identifier.

With iOS13 and Swift 5.5, the long-awaited Async / Await was introduced. It was nice of Apple that they recognized the issue that when something new is introduced then a long delay is happening till it can be used on production as we usually need to support more iOS versions.

Async / Await is a way to run asynchronous code without completion handlers.

Simplest Async / Await

Here is some code worth mentioning:

  • Task.isCancelled
  • Task.init(priority: .background) {}
  • Task.detached(priority: .userInitiated) {}
  • Task.cancel()

I would highlight TaskGroup. This is the “DispatchGroup” in the Await / Async world. I have found that Paul Hudson has a really nice example on this link.

Nice Example of TaskGroup by Paul Hudson

Actors are classes, reference types that are thread-safe. They handle data race and concurrency issues. As you can see below, accessing the property of the actor is done with await keyword.

“Actors allow only one task to access their mutable state at a time.” — Apple Docs

Example from https://docs.swift.org/

We’ve covered lots of multithreading topics — from UI and Background Thread to Deadlocks and DispatchGroup. But I’m sure you are now on your way to being an expert or at least prepared for iOS interview questions about multithreading topics.

The whole code sample can be found on the next link: GitHub. I hope it will be valuable to play with it yourself.

Leave a Comment