Elapsed | Time | Activity |
---|---|---|
0:00 | 0:05 | Objectives |
0:05 | 0:20 | Initial Exercise |
0:25 | 0:25 | Overview I |
0:50 | 0:10 | BREAK |
1:00 | 0:25 | Overview II |
1:25 | 0:25 | In Class Activity I |
TOTAL | 1:50 |
By the end of this lesson, you should be able to...
- Identify and describe:
- when to use Synchronous and Asynchronous
- Sync and Deadlocks
- Critical Section
- Thread Safety
- GCD's provided serial and concurrent DispatchQueues
- QoS Priority
- how the Main Queue fits in GCD
Scenario:
- Your app fetches images from the Internet, then processes them for display through a sepia tone filter.
- Fetching and processing images is negatively impacting performance, especially scrolling in the table view.
TODO: Diagram how you would use what you know so far about currency in iOS to improve your app's performance. In your diagram, be sure to point out:
- the point in time on the relevant queue(s) where an image is downloaded
- the point where the image is filtered
- the point at which the image is presented to the user, and the queue on which this would take place
Let's examine the code in the snippet below:
func someFunction() {
DispatchQueue.global().async { // A) Run the following code block asynchronously on a non-UI thread
let url = URL(string: imageUrl)
do {
let data = try Data(contentsOf: url!)
DispatchQueue.main.sync { // B) Run the code below synchronously on the main queue
self.imageIcon.image = UIImage(data: data)
}
}
}
Q: What exactly is happening in this code?
Q: When might this be useful?
Source: https://www.reddit.com/r/iOSProgramming/comments/7n9e9f/what_is_the_difference_between/
In asynchronous execution by the .async
method, the method call returns immediately, ordering the task to be done but not waiting for it to finish.
Asynchronous tasks are started by one thread but actually run on a different thread, taking advantage of additional processor resources to finish their work more quickly.
from: Apple docs
Use .async
when your app does not need to wait until the operation inside the block is finished.
As mentioned, long-running tasks such as image/data processing, local or remote data fetching, and other network calls are typically good candidates for running tasks .async
.
If the viewDidLoad()
method of your View Controller
has too much work to do on the main queue
, this can often result in long wait times before the view appears. If possible, it’s best to offload some work to be done in the background using .async
, if it’s not absolutely essential at load time for the view.
In synchronous execution by .sync
method, the current thread waits until the task is finished before the method call returns.
Use .sync
when your app needs to wait until a task is finished.
Example scenarios include:
- to ensure a particular function is not called a second time before its first invocation is finished.
- to wait for an operation inside a block to finish before you can use the data processed by the block.
Do not call the dispatch_sync
(aka, .sync
) function from a task that is executing on the same queue that you pass to your function call. Doing so will deadlock the queue.
If you need to dispatch to the current queue, do so asynchronously using the dispatch_async
(.async
) function.
Source: Apple docs
A deadlock occurs when two or more tasks are waiting on each other to finish and get stuck in a never-ending cycle. Neither can proceed until the other completes; but, since neither can proceed, neither will finish.
A deadlock can occur even when the perpetually-waiting tasks are on the same thread.
We'll see an example of a deadlock a bit later in this lesson....
When your app starts, a main
dispatch queue is automatically created for you.
The main queue
:
- is a serial queue that's responsible for your UI. (We'll cover serial and concurrent queues in the next lesson.)
- is closely associated with the
main thread
and its call stack. Themain queue
only executes code on themain thread
.
REMEMBER: You should never perform UI updates on any queue other than the
main queue
. And you must always access UI Kit classes on themain thread
.
Our updated diagram of the structure inside the runtime process of an iOS app simply illustrates that the main queue
is associated directly with the main thread
and its call stack, and that the system also creates queues for non-UI tasks.
(1) When an iOS app starts, the system automatically creates the app's:
main queue
main thread
- and the corresponding call stack that the
main thread
manages.
(2) The main thread
, again, 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 frames, and so on...
(3) Notice that, though the system also creates a pool of additional threads for potential non-UI tasks and their corresponding call stacks, what actually happens is here is that additional dispatch queues are created (to which the system will assign a thread from the pool, as needed):
Because it's used so often, Apple has made it available as a class variable, which you access via DispatchQueue.main
.
Example showing .async
called on the built-in DispatchQueue.main
property:
import Foundation
var value: Int = 2
DispatchQueue.main.async {
for i in 0...3 {
value = i
print("\(value) ✴️")
}
}
for i in 4...6 {
value = i
print("\(value) ✡️")
}
DispatchQueue.main.async {
value = 9
print(value)
}
In general, look for opportunities to take long-running, non-UI task and run them asynchronously on a queue other than the main queue
.
The pseudocode example below illustrates the typical steps to running a non-UI tasks asynchronously on a background queue/thread:
- create a queue
- submit a task to it to run asynchronously
- when it is complete, you redirect control flow back to the
main thread
to update the UI.
// Somewhere inside a class...
let queue = DispatchQueue(label: "com.makeschool.queue")
// Somewhere in your function
queue.async {
// Call slow non-UI methods here
DispatchQueue.main.async {
// Update the UI here
}
}
Just as with the current queue, you never want to execute a code block synchronously against the main queue
either. Doing so could cause your app to crash or it might simply degrade your app's performance by locking your UI.
For example, this statement:
DispatchQueue.main.sync {
// Some synchronous code block
}
...will cause the following events:
sync
queues the block in themain queue
.sync
blocks the thread of themain queue
until the block finishes executing.- but
sync
will wait forever because the thread where the block is intended to run is blocked. The submitted code block will not even start until the thread is unblocked, and the thread can't be unblocked until that same code block completes.
In the diagram below:
- A code block identified as
Code Block A
was submitted synchronously to theMain Queue
. - At the point in which
DispatchQueue.main.sync
is called, theMain Queue
is blocked awaiting the completion of the code block (Code Block A
) which called.sync
. - But
Code Block A
cannot even start executing until theMain Queue
is unblocked.
The key to understanding this is that .sync
does not execute tasks/blocks, it only queues them. Execution will happen on a future iteration of the run loop. If the thread is blocked until some condition occurs (i.e., some block of code completes), no future iteration of the run loop can occur that condition is met and the queue and its thread are unblocked.
Source: https://medium.com/swift-india/parallel-programming-with-swift-part-1-4-df7caac564ae
Before we delve deeper into GCD's DispatchQueues
, let's explore a couple of related concepts...
Multiple, concurrent accesses to shared resources can lead to unexpected or erroneous behavior. So, parts of the program where the shared resource is accessed are protected from concurrent access.
This protected section is called the critical section (or critical region).
The code in the critical section:
- accesses shared variables/resources and has to be executed as an atomic action.
- should not be executed by more than one process at a time.
Typically, the critical section accesses a shared resource — such as a data structure, a peripheral device, or a network connection — that would not operate correctly in the context of multiple concurrent accesses.
For example, a critical section might manipulate a particular variable that can become corrupt if it is accessed by concurrent processes.
In the diagram below, if Process 1
executes the code in the Critical Section to read a shared variable — while Process 2
needs to write to the same variable — Process 1
might get either the old or new value of the variable:
Examples:
- Classic Example — A bank account shared by two people.
If Person 1
checks the account balance at the same time that Person 2
executes some transaction (eg., withdraws money), that transaction may not be reflected in the balance that Person 1
sees.
To ensure the balance reported is always accurate, any code accessing the variable holding the balance can be protected from concurrent access.
- A shared document.
When two or more people are updating a shared document, access to the document can be temporarily limited to each active contributor — even if only for an instance — to prevent additional contributors from making changes before the doc is autosaved.
Sources:
- Apple
- wikipedia
Thread safe code:
- can be safely called from multiple threads or concurrent tasks without causing any problems (data corruption, crashing, etc).
- is guaranteed to be free of race conditions when accessed by multiple threads simultaneously.
An example of thread safe code: A Dictionary
or an Array
that is declared as a constant (with let
) — because it is read-only,, you can access it from multiple threads at the same time without issue.
Code that is not thread safe must only be run in one context at a time — it must not be accessed from more than one thread at a time.
Example: A Dictionary
or an Array
declared as a var
is not thread safe and should only be accessed from one thread at a time.
GCDs DispatchQueues
possess several defining attributes:
- Queues can be either serial or concurrent
- FIFO — This guarantees that the first task added to the queue is the first task started in the queue, the second task added will be the second to start, and so on...
- Thread Safe — All dispatch queues are themselves thread-safe: you can access them from multiple threads simultaneously. On of the key benefits of GCD is that
DispatchQueues
can provide thread-safety to parts of your own code.
Remember: The decision of when to start a task is entirely up to GCD.
- If the execution time of one task overlaps with another, it’s up to GCD to determine if it should run on a different core, if one is available, or instead to perform a context switch to run a different task.
Serial queues guarantee that only one task runs at any given time.
Serial Queues:
- only have a single thread associated with them and thus only allow a single task to be executed at any given time.
- execute tasks in the order they are submitted, one at a time. 1
Since no two tasks in a serial queue can ever run concurrently, there is no risk they might access the same critical section concurrently; that protects the critical section from race conditions with respect to those tasks only. So if the only way to access that critical section is via a task submitted to that dispatch queue, then you can be sure that the critical section is safe.
Source:
https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
The Main Queue is a Serial Queue
As mentioned, when an iOS app launches, the system automatically creates a serial queue called the main queue
and binds it to the application’s main thread
.
Any tasks assigned in the main queue
and are executed serially, one at a time, on the main thread
in their turn.
Any time you write code, such as the following snippet, to access the main queue
you are accessing the same, singular system-created queue, and any code you submit to that queue is executed serially on the main thread
:
let mainQueue = DispatchQueue.main
This main queue
is the queue to use for sending messages to UIViews and and all other UI-related tasks.
On the other hand, a concurrent queue is able to utilize as many threads as the system has resources for.
For Concurrent Queues:
- threads will be created and released as needed for a concurrent queue.
- multiple tasks can run at the same time.
- tasks are guaranteed to start in the order they were added.
GCD and Concurrent Queues 1
But tasks can finish in any order and you have no knowledge of the time it will take for the next block to start, nor the number of blocks that are running at any given time. (This is entirely up to GCD.)
The decision of when and where to start a block is entirely up to GCD, too. If the execution time of one block overlaps with another, it’s up to GCD to determine if it should run on a different core, if one is available, or instead to perform a context switch to a different block of code.
If your iOS device is completely bogged down and your app is competing for resources, it may only be capable of running a single task.
Source:
https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
1 GCD controls the execution timing. You won’t know the amount of time between one task ending and the next one beginning.
The GCD library (libdispatch
) automatically creates several queues with different priority levels that execute several tasks concurrently, selecting the optimal number of tasks to run based on the operating environment.
GCD provides three main types of queues:
-
The Main Queue — a serial queue that runs on the
main thread
. -
Global Dispatch Queues — These a concurrent queues provided by the system which the system shares with any processes requesting them. There are four Global Dispatch Queues with different priorities:
- high
- default
- low
- background
- Custom Queues — Queues you create. You can create them as either serial or concurrent queues.
- In actuality, the system will assign one of the Global Queues to handle your custom queue's activities.
When setting up Global Dispatch Queues, you do not specify the priority directly; instead, you are required to specify a Quality of Service (Qos) level (as a class property) which guides GCD into determining the priority level to give the task.
GCD offers you four Quality of Service (Qos) classes:
-
.userInteractive
-
.userInitiated
-
.utility
-
.background
For the QoS class assigned to your team, fill in the following categories (columns) in the slide provided:
- Type of work and focus of QoS
- Duration of work to be performed
- Example Use Case
Team 1: .userInteractive
Team 2: .userInitiated
Team 3: .utility
Team 4: .background
HINT: See Resources below for details
Resources:
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html
https://developer.apple.com/documentation/dispatch/dispatchqos
Q: Does Asynchronous mean Concurrent?
- Research:
- The Dining Philosophers Problem
- The Critical Section Problem
- Race Condition(s)
- The two secondary GCD QoS priority classes:
-
.default
-.unspecified
- Assignment:
- Execute the following tutorial (part 1 only):
https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
- Start working on assigned current tutorial
- Complete reading