I am Boris Dobretsov, and this is the third part of a series Understanding Parallel Programming: A Guide for Beginners.
If you haven’t read the first two parts, take a look before going on:
Understanding Parallel Programming: A Guide for Beginners,
Understanding Parallel Programming: A Guide for Beginners, Part II.
If you have already read the first two parts, then today we are going to talk about Threads.
There are three mechanisms for managing threads in iOS:
• Thread - this class directly represents a thread. It allows you to create and stop threads and is the fundamental tool for organising multithreading in iOS.
• Grand Central Dispatch (GCD) - a threading library built on top of Thread. It abstracts away the complexity of working with threads by letting you manage tasks through queues and blocks of code, which are executed on those queues.
• Operation - a higher level library than GCD. It uses queues and special Operation classes.
GCD and Operation will be discussed in the next articles, while today we will take a closer look at Thread.
Using the Thread class in the fundamental way of managing threads in iOS. Although it is not commonly used now, the understanding of how it works is essential for using other technologies that are build on top of it.
First of all let’s look at the thread without which an app cannot be created - the main thread.
The main thread has a special role: it processes the user interface (UI). This means that everything displayed on the screen goes through the main thread. No other thread is allowed to interact with the UI. If you attempt to modify the UI from a secondary thread, the best-case scenario is a warning, and the worst-case scenario is an app crash. There’s also an intermediate possibility: the app might produce errors, causing UI elements to be displayed incorrectly.
By default, all the code you write runs in the main thread, but relying on this for all tasks is a bad practice. The more resources your tasks consume in the main thread, the slower the UI becomes. Everyone has experienced apps where table scrolling or screen transitions flickers or lags - this happens because of excessive load on the main thread.
Another important point about the main thread is that it’s the only thread with a RunLoop running by default. This means secondary threads won't automatically handle asynchronous code, nor can you set timers in them. If you need to use a RunLoop in a secondary thread, you’ll have to explicitly start it.
Now we know enough to start creating our own threads. Let's start with an example of regular single-threaded code. We'll write two loops, both 10 iterations long. The first one prints a devil to the console, the second one prints an angel.
for _ in (0..<10) {
print("😈")
}
for _ in (0..<10) {
print("😇")
}
After executing the code in the console, we will see that first the devils were displayed, then the angels.
😈
😈
😈
😇
😇
😇
This is an expected behaviour. In our code, each of the loops can be thought of as a separate task. In this case, first task is executed, and then the second one. If we need to execute these tasks simultaneously, we need to send one of them to another thread. There are several ways to do this, let's start with the simplest.
The easiest way to execute a task in another thread is to call the detachNewThread
method of the Thread class and pass it a closure with your task. This will immediately create a new thread to execute it. The downside of this approach is that you won't be able to control this new thread, but this is not always necessary.
Thread.detachNewThread {
for _ in (0..<10) {
print("😈")
}
}
for _ in (0..<10) {
print("😇")
}
In this example, the console output will change:
😇
😈
😇
😈
😇
😈
😇
😈
😇
😈
😇
😈
😇
😈
😇
😈
😇
As you can see, angels and demons interchange. This is an obvious sign that the tasks are executed simultaneously. After the code block is executed, the thread will be closed.
The second way to create a new thread is similar to the first one, but it is already considered obsolete - it should only be used if iOS versions below 10 are supported. This is a call to the detachNewThreadSelector
method of the Thread class and passing it a selector to a method of some object. Accordingly, the task code should be moved to a separate method. It should be marked with the @objc
keyword, since this entire procedure has its roots in the Objective-C.
@objc func printDemon() {
for _ in (0..<10) {
print("😈")
}
}
Thread.detachNewThreadSelector(#selector(self.printDemon), toTarget: self, with: nil)
for _ in (0..<10) {
print("😇")
}
The output in the console will be exactly the same as in the previous case.
The third method is more universal. We will create an instance of the Thread class using a constructor that accepts a code block. This way, we will have a thread object that we can manage. The important detail of this approach is that the thread is started manually using the start
method.
let thread1 = Thread {
for _ in (0..<10) {
print("😈")
}
}
thread1.start()
for _ in (0..<10) {
print("😇")
}
You can also use the legacy version of this method - create a stream object through a constructor with a selector.
let thread1 =Thread(target: self, selector: #selector(self.printDemon), object: nil)
The last way is to create your own thread subclass. In this case, you need to override the main method, since it will be called when the thread starts. This method should be used to encapsulate complex logic to make the program more readable.
class ThreadprintDemon: Thread {
override func main() {
for _ in (0..<10) {
print("😈")
}
}
}
let thread1 = ThreadprintDemon()
thread1.start()
for _ in (0..<10) {
print("😇")
}
Today, we discussed the basics of threading in iOS, starting with the Thread class. Although not commonly used now, understanding of Thread is essential for working with higher-level technologies like GCD and Operations. We explored the role of the main thread in managing the UI and the importance of not overloading it to avoid performance issues. We also discussed how secondary threads work and different ways to create and manage threads: using detachNewThread
, detachNewThreadSelector
, and subclassing Thread
.
Next time we will continue exploring Thread with Thread management, Thread execution flags and Thread priority management tool.
Stay tuned!