I am Boris Dobretsov, and some time ago I published my first article -
In that article, I introduced basic concepts such as "processes" and "threads" and explained in simple terms what a processor, operating system, process, and thread are.
Today, we will elaborate further on RunLoops and their impact on asynchronous tasks.
But first, let's refresh our knowledge of what a RunLoop is and what it is responsible for.
A RunLoop helps asynchronous tasks run at the right time without blocking or interfering with the main thread. It functions as a cycle of event handling, used to schedule tasks and coordinate incoming events. Its purpose is to keep the thread active when there is work to do and put it to sleep when there is none.
As we know from the previous article, there are several main types of architecture for code organization:
Synchronous single-threaded
Asynchronous single-threaded
Synchronous multi-threaded
Asynchronous multi-threaded
Some of these are simpler, while others are designed to run several tasks (competing tasks) at the same time, making the development process more efficient and multifunctional. The most commonly used architecture in iOS application development is the asynchronous multi-threaded model. It is important to understand how to interact with these models and what to do to prevent overloading. That is why in this article we are going to discuss RunLoops impact on asynchronous tasks.
As we also know from the previous article, asynchronous tasks can be paused mid-execution and resumed later.
But who decides when to put a task on hold and when to resume it?
It’s actually the task itself that makes this decision. This is because asynchronous tasks consist of two parts: the first starts the task, and the second executes asynchronously in response to a specific event. The latter part is added to a source for the RunLoop and is checked on each loop iteration. If the specified event occurs, the task proceeds.
A basic example of an asynchronous task is a timer. Let me share the code which which demonstrates this idea:
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
print("tick")
}
In the above example, you can see the code which starts a timer that runs every quarter of a second, outputting "tick" to the console.
The first part of the asynchronous task is creating the timer, and the second part is passing the block to the scheduled Timer method. The task is single-threaded: both the timer setup and block execution happen in the same thread. However, it is asynchronous since the block doesn’t execute immediately but after a delay that allows the thread to do other work in the meantime.
This illustrates the interconnection between asynchronous tasks and the RunLoop. The timer block will be called every quarter second, with the RunLoop responsible for this condition. However, if the thread is busy with a long-running task and the iteration cannot be completed within 0.25 seconds, the timer's block will be delayed.
Let's modify the example so that the block outputs the current time:
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
print(Date())
}
Let’s see what will be the console output:
2017-09-26 05:13:16 +0000
2017-09-26 05:13:16 +0000
2017-09-26 05:13:16 +0000
2017-09-26 05:13:16 +0000
2017-09-26 05:13:17 +0000
2017-09-26 05:13:17 +0000
2017-09-26 05:13:17 +0000
2017-09-26 05:13:17 +0000
2017-09-26 05:13:18 +0000
2017-09-26 05:13:18 +0000
2017-09-26 05:13:18 +0000
2017-09-26 05:13:18 +0000
In this case, the timer goes off four times per second. Let's now add another asynchronous task that runs once every second and takes up the thread by waiting to be executed:
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { timer in
print(Date())
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
sleep(1)
}
Console output:
2017-09-26 05:17:33 +0000
2017-09-26 05:17:34 +0000
2017-09-26 05:17:34 +0000
2017-09-26 05:17:34 +0000
2017-09-26 05:17:35 +0000
2017-09-26 05:17:35 +0000
2017-09-26 05:17:36 +0000
2017-09-26 05:17:36 +0000
2017-09-26 05:17:36 +0000
2017-09-26 05:17:37 +0000
2017-09-26 05:17:37 +0000
2017-09-26 05:17:38 +0000
2017-09-26 05:17:38 +0000
2017-09-26 05:17:38 +0000
2017-09-26 05:17:39 +0000
2017-09-26 05:17:39 +0000
2017-09-26 05:17:40 +0000
2017-09-26 05:17:40 +0000
As you can see, the timer now triggers at irregular intervals, sometimes twice, sometimes three times per second. This happened because the thread was too busy running other code to process the timer in time.
This example perfectly illustrates that there is no magic in asynchronous code. It relies on the RunLoop and competes with other tasks in the thread. My advice is: don’t rely on miracles when planning asynchronous tasks; consider whether they can be executed on time. If there is a likelihood that thread resources may be insufficient to run all tasks, consider offloading tasks to separate threads where they won’t face interference.
In the next article, we will talk about the main approaches to building multi-threaded code in iOS and will touch upon the concepts of Thread, Grand Central Dispatch (GCD), and Operation. Stay tuned!