By the end of this lesson, you should be able to...
- Describe:
- Why Concurrency is important in iOS
- The relationships between processes, threads, and tasks, and how they fit together at the launch of any iOS application
- The similarities and key differences between Parallelism and Concurrency
- How you could have Concurrency without Parallelism and vice-versa
- (at a very high level) Apple's primary API for managing Concurrency - Grand Central Dispatch (GCD)
- Identify:
- the five major challenges associated with Concurrency
Q: Why do apps need concurrent activities?
A: To keep the UI responsive.
When you create a new iOS app, the app acquires its main
thread. That main
thread is responsible for running all of the code that powers the app's user interface.
As you add code on your main
thread to perform large items of non-UI work — such as image processing or fetching and transforming data — you will find that your UI's performance suffers drastically.
Your user interface will slow down, or maybe even stop altogether.
A common example:
- A table view that will not scroll properly while the app is downloading and transforming images; scrolling stutters and you might need to display multiple "busy" indicators of the expected images.
The concept of Concurrency in iOS is about how to structure your app to avoid such UI performance issues by directing slow, non-UI tasks to run somewhere other than on the UI thread (aka, the main
thread).
Concurrency issues loom large in any list of the top mistakes made by iOS developers. They are also the underlying cause of the majority of negative app reviews.
Thus it is not surprising that questions on iOS concurrency are now a standard part of the technical interview process for iOS development jobs.
Key concepts covered in this course will include:
- Process
- Thread
- Task
- Multi-Core Systems
- Concurrency
- Parallelism
- Queues (Serial, Concurrent)
- Synchronous vs Asynchronous
- Grand Central Dispatch (GCD)
- Background Tasks
- Quality of Service (QoS)
- Operations
- Dispatch Groups
- Semaphores
- Debugging
- Testing Strategies
...we will cover a few of the most essential concepts today...and the rest, we'll cover later in the course...
Process — The runtime instance of an application. A process has its own virtual memory space (aka, virtual machine) and system resources (including port rights) that are independent of those assigned to other programs.
- A process always contains at least one thread (the main thread) and may contain any number of additional threads.
Thread — A flow of execution inside a process. A thread of execution is the smallest sequence of programmed instructions that can be managed independently by the operating system's scheduler.
-
Each thread comes with its own stack space but otherwise shares memory with other threads in the same process.
-
A thread defines a discrete mechanism, within a single process, for executing tasks.
-
Threads can execute concurrently, but that is up to the operating system.
Comparing Processes to Threads
Processes | Threads |
---|---|
Are typically independent | Threads exist as subsets of a process |
Have separate address spaces | Threads share their address space with other threads in the same process |
Carry considerably more state information than threads | Multiple threads within a process share process state as well as memory and other resources |
Task — A quantity of work to be performed.
A task is simply some work that your application needs to perform (i.e., some block of code to execute).
For examples, you could create a task to perform some calculations, blur an image, create or modify a data structure, process some data read from a file, convert JSON data, or fetch data from local/remote sources.
Sources:
- Wikipedia
- Apple Concurrency Programming
Tasks run on threads...
- The UI (and all UI-related tasks) runs on the Main thread, which is automatically created by the system.
- The system also creates other threads for its own tasks. Your app can use these threads...or create its own threads.
Parallel programming utilizes a shift from procedural tasks, which run sequentially, to tasks that run at the same time.
In Parallel Computing:
-
Many calculations or the execution of processes are carried out simultaneously.
-
A computational task is typically broken down into several very similar sub-tasks that can be processed independently and whose results are combined after all tasks are completed.
Note that there are several different forms of Parallel Computing: bit-level, instruction-level, data, and task parallelism.
Concurrency refers to the ability to decompose a program, algorithm, or problem into smaller components or units that can be executed out-of-order, or in partial order, without affecting the final outcome.
Concurrency is the act of dividing up work.
This allows for parallel execution of the concurrent units, which can significantly improve overall speed of execution on multi-processor and multi-core systems.
A recent trend in computer architecture is to produce chips with multiple cores (CPUs) on a single chip, a trend driven by concerns over excessive heat generated by increased power consumption.
With the advent of modern multi-core CPUs, Parallel Computing has become the dominant paradigm in computer architecture due to its potential to optimize performance.
Multi-core devices execute multiple threads at the same time via Parallelism.
Diagram of a generic dual-core processor with CPU-local level-1 caches
and a shared, on-die level-2 cache.
Source:
CountingPine at English Wikipedia - Public Domain,
https://commons.wikimedia.org/w/index.php?curid=11894458
Parallel Computing is closely related to Concurrent Computing (in fact, Concurrent Computing is an example of task parallelism.)
Concurrency is about structure, while Parallelism is about execution.
Though both are frequently used together, and often conflated, the two concepts are distinct:
- it is possible to have parallelism without concurrency (such as bit-level parallelism)
- it is also possible to have concurrency without parallelism (such as multitasking by time-sharing on a single-core CPU).
Tasks run on threads.
But for threads to execute tasks concurrently, must multiple threads run at the same time?
Single-core devices can achieve Concurrency through time-slicing, in which the OS uses "context switching" to alternate between multiple threads.
For a multi-threaded application running on a traditional single-core chip, the OS would run one thread, perform a context switch, then run another thread, as illustrated in the first diagram below where thread 1 (T1) pauses while threads 2 thru 4 run, then thread 1 resumes, etc.:
On a multi-core chip, the threads could be spread across all available cores, allowing true parallel processing, as shown here:
Source:
https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html
Let's play the Movie Theatre Game...
There can be as many threads executing at once as there are cores in a device's CPU.
iPhones and iPads have been dual-core since 2011, with more recent models boasting as many as 12 cores per chip (see 1).
With more than one core available, iOS apps are capable of running more than a single task at the same time. (Potentially, up to 8 tasks simultaneously, though this again is ultimately up to the operating system).
Below is a simplified diagram of the structure inside the runtime process
(aka, virtual machine
) of an iOS app at launch: the moment the user taps the app icon.
(1) When an iOS app starts, the system automatically creates the app's main thread
and the corresponding call stack
that the main thread
manages.
(2) The main thread
eventually (after executing required Cocoa Touch functions) allocates your app's Application
object in its stack frame
, which in turn executes its delegate methods on its AppDelegate
object in their respective stack frame
s, then the AppDelegate
begins creating all of your app's user interface components and behavior.
From that point on — and until the Application
object's lifecycle (run loop) ends — all UI-related code in your app will execute on the main thread
.
-
This behavior ensures that user-related events are processed serially in the order in which they were received.
-
But unless specified otherwise, all non-UI code will also execute on the
main thread
(exceptions to this include frameworks such asURLSession
in which some tasks run on non-UI threads by default).
(3) Meanwhile the system also creates additional threads (nonUI threads), along with their corresponding call stack
s, making them available for use by your app.
Splitting your app into logical "chunks" of code enables iOS to run multiple parts of your app across more than one core at the same time, which can greatly improve overall performance.
In general, look for opportunities to structure your apps so that some tasks can run simultaneously: Determine which pieces can run at the same time — and possibly in random order — yet still result in a correct implementation of your data flow for your users.
Tasks which are good candidates to run simultaneously from different threads typically fall into these categories:
- tasks that access different resources
- tasks that only read values from shared resources
Important Note: Tasks which modify the same resource must not run at the same time, unless the resource is
threadsafe
(we'll coverthread safety
later in the course)
Most modern programming languages provide some form of Concurrency, but different languages use widely disparate mechanisms for handling it.
Swift and iOS provide two APIs you can use to improve your app's performance through Concurrency:
- Grand Central Dispatch — commonly known as GCD (also simply called "Dispatch" by Apple).
- Operations — which are built on top of GCD.
Grand Central Dispatch (GCD) is a low-level API for managing concurrent operations.
Named after Grand Central Station in New York City, GCD was released by Apple in 2009 to optimize application support for systems with multi-core processors and other symmetric multiprocessing systems.
It is an implementation of task parallelism based on the Thread Pool design pattern.
The fundamental idea is to move the management of the thread pool out of the hands of the developer, and closer to the operating system.
GCD offers you an efficient mechanism for executing code concurrently on multicore hardware by submitting work to dispatch queues managed by the system rather than working with threads directly.
In the next lessons, we will dig deeper into these two Apple concurrency frameworks, including learning more about the differences between GCD and Operations, as well as when to choose one over the other...
By now, you've likely gotten the idea that Concurrency can significantly alleviate performance issues for you.
But it isn't free.
Concurrency presents its own specific development challenges.
In this course we will explore the following set of the most major challenges associated with Concurrency, along with standard approaches to avoid or resolve them:
- Deadlocks
- Race Conditions
- Readers-Writers Problem
- Thread Explosions
- Priority Inversion
Before we delve deeper into GCD in the next lesson, let's explore a simplified example of what implementing Concurrency without GCD might entail...
The code in the Threads.playground below is incomplete. It is intended to create a second instance of the Thread
class named "Background Thread" that executes the calculation
function/closure.
TODO: Complete the code to match the output listed below it:
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let calculation = {
for i in 0...100 {
print(i)
}
}
let thread = Thread {
//TODO: What must the thread do here to match the expected output listed below?
}
print("On thread: \(Thread.current) doing nothing")
//TODO: Give new thread its proper name, as in expected output...
thread.qualityOfService = .userInitiated
thread.start()
/* EXPECTED OUTPUT:
On thread: <NSThread: 0x6000022d28c0>{number = 1, name = main} doing nothing
On thread: <NSThread: 0x6000022fba00>{number = 3, name = Background Thread} doing work
0
1
2
3
4
5
6
7
8
9
10
11
...
100
*/
TODO: Trace down the source of the Foundation types Thread
, Thread.current
, and the .start()
function.
- Is it easy to infer how to implement these properties and functions?
Q: Listed below is a selected portion of the output:
- What do the hexadecimal numbers next to
<NSThread:
tell us and how could that information be useful? - Where did the properties
number
andname
come from?
On thread: <NSThread: 0x600003af0dc0>{number = 1, name = main} doing nothing
On thread: <NSThread: 0x600003acc340>{number = 3, name = Background Thread} doing work
Q: This approach involved direct creation and management of threads.
- What drawbacks do you foresee with this approach, especially in more complex implementations?
- Research:
- Task Parallelism
- Bit-Level Parallelism
- Amdahl's Law and Gustafson's Law
- Call Stack, Stack Frames, and Stack Pointer
- The Heap
- Thread Pool design pattern
- Scheduler (for iOS thread scheduling)
- Run Loop
- Async/Await pattern (and Swift 5.0)
- Nonatomic (vs Atomic)
- Dispatch Queues
- Quality of Service (QoS) Priority - as defined by Apple for iOS/macOS
- Complete reading / research
- Slides
- Parallel computing - wikipedia
- Concurrency (computer_science) - wikipedia
- Threads - an article
- Processes and Threads - Apple
- Apple-designed_processors - Apple 1
- Dispatch - from Apple
- Grand_Central_Dispatch - wikipedia 2
- The App LifeCycle - Apple
- Context switch - wikipedia
- Thread safety - wikipedia
- Call Stack