Elapsed | Time | Activity |
---|---|---|
0:00 | 0:05 | Objectives |
0:05 | 0:30 | Initial Activity |
0:35 | 0:30 | Overview I |
1:05 | 0:10 | BREAK |
1:15 | 0:30 | In Class Activity I |
TOTAL | 1:45 |
By the end of this lesson, you should be able to...
- Identify and describe:
- Apple's 2 APIs for managing Concurrency - Grand Central Dispatch and Operations
- queues and dispatch queues
- FIFO Queues
- Synchronous and Asynchronous tasks
- the lifecycle and other attributes of the Main Queue
- How to implement:
- basic examples of dispatch queues running built-in
.sync
and.async
functions
Discuss as a class...
In the Movie Theatre Game from Lesson 1:
-
What physical construct (ie., customer line, casher, etc.) represented:
- A process?
- A thread?
- A long-running task? -
Which aspect of your solution represented:
- Concurrency?
- Parallelism?
Follow up for Lesson 1's Activity 2
-
Review of Concurrency & Parallelism concepts from Lesson 1...
-
Questions from Last Lesson:
- How is it possible to have parallelism without concurrency?
Let's examine of few of the types of Parallelism...
Bit-level Parallelism: Can be thought of as hardware-based parallelism.
Historically, 4-bit microprocessors were replaced with 8-bit, then 16-bit, then 32-bit microprocessors, and now 64-bit CPUs, each capable of processing twice the number of instructions per cycle as the one before.
Because the number of instructions the system must run to execute a particular task on a 64-bit processor is significantly reduced (compared to previous processors), it can be said that the same number of instructions can now be executed in parallel.
However, there is no concurrency in this case because it does not involve changes to the structure of the executed task.
Data Parallelism: — is parallelism inherent in program loops, which focuses on distributing the data across different computing nodes to be processed in parallel.
In Data Parallelism, the same calculation is performed on the same or different sets of data.
Example:
A Single instruction, multiple data (SIMD) is a class of parallel computers... with multiple processing elements that perform the same operation on multiple data points simultaneously.
Such machines exploit data level parallelism, but not concurrency: there are simultaneous (parallel) computations, but only a single process (instruction) at a given moment.
SIMD is particularly applicable to common tasks such as adjusting the contrast in a digital image or adjusting the volume of digital audio. Most modern CPU designs include SIMD instructions to improve the performance of multimedia use.
In the diagram of a SIMD system below, the same instruction (task) from the Instruction Pool is sent via the four PUs (Processing Units) for execution on the same data object (the Data Pool).
Again, because there is no manipulation of the structure of the task, there is no concurrency involved:
Task Parallelism: — is the characteristic of a parallel program that entirely different calculations can be performed on either the same or different sets of data.
This contrasts with Data Parallelism, where the same calculation is performed on the same or different sets of data.
Note that Concurrency is a type of Task Parallelism where tasks are divided (decomposed) into smaller bits for parallel processing.
And now...
...a small Quiz...
- GCD vs Operations
- Dispatch Queues
- FIFO
- Synchronous vs Asynchronous
- The Main Queue
In Lesson 1, we introduced the two Apple-provided APIs you use in iOS to manage concurrent tasks without working with threads directly: Grand Central Dispatch (GCD) and Operations.
Before we dive deeper into GCD, let's quickly compare the two to begin your understanding of when and how to use them:
Grand Central Dispatch | Operations |
---|---|
A lightweight way to represent units of work that are going to be executed concurrently. GCD uses closures to handle what runs on another thread. | Operations are objects that encapsulate data and functionality. Operations add a little extra development overhead compared to GCD. |
The system takes care of scheduling for you. Adding dependencies, cancelling or suspending code blocks is labor intensive | Allow for greater control over the submitted tasks, including some control over scheduling through adding dependencies among various operations and can re-use, cancel or suspend them. |
GCD is good to use for simple, common tasks that need to be run only once and in the background. | Operations make it easier to do complex tasks. Use them when you need (a) task reusability, (b) considerable communication between tasks, or (c) to closely monitor task execution. |
Apple recommends using the API with the "highest-level of abstraction" (which is Operations), though most developers use a combination of both APIs.
And because Operations are build on top of GCD, it is important to master the lower-level API first...
Note: Where Swift uses closures (functions) to handle the code that runs on another thread, C#, Typescript, Python, JavaScript and other languages use the the more common Async/Await pattern. The original plans for Swift 5.0 included adding the Async/Await pattern, but this was removed from the Swift specification until some future release.
GCD's design improves simplicity, portability and performance.
-
It can help you improve your app’s responsiveness by deferring computationally expensive tasks from the foreground (
main
thread) to the background (non-UI threads). -
It simplifies the creation and the execution of asynchronous or synchronous tasks.
-
It’s a concurrency model that is *much easier to work with* than locks and threads.
Though GCD still uses threads in its implementation, developers do not have to manage threads themselves.
GCD's tasks are so lightweight to enqueue that Apple, in its 2009 technical brief on GCD, stated that "only 15 instructions are required for implementation, whereas creating traditional threads could require several hundred instructions." 1
GCD is Apple's implementation of C's libdispatch
library. It runs directly in the UNIX layer of iOS.
GCD works by allowing specific tasks — functions or closures — that can be run in parallel to be queued up for execution and, depending on availability of processing resources, schedule them to execute on any of the available processor cores (referred to as "routing" by Apple). 1
GCD abstracts the notion of threads, and exposes dispatch queues to handle work items (work items are blocks 2 of code that you want to execute). These tasks are assigned (dispatched) to a dispatch queue, which processes them in a First-In-First-Out (FIFO) order.
Allowing the libdispatch
library and the operating system to manage threads means developers have much fewer lines of code to write and less to debug; and the library can optimize thread management behind the scenes much more efficiently than a developer.
2 Note: Apple's documentation sometimes refers to a
block
in lieu of aclosure
becauseblock
was the name used in Objective-C. In the context of concurrency in iOS, you can considerblock
andclosure
interchangeable> [action]
Sources include:
- wikipedia
- Apple docs
Grand Central Dispatch still uses threads at a low level but abstracts them away from the developer.
You work with threads by creating DispatchQueues
.
DispatchQueues
In Computer Science, a queue
is a data structure that manages a collection of objects in FIFO 3 order, where the first object added to the queue is the first object removed from (executed by) the queue.
Photo credit: FreeImages.com/Sigurd Decroos
In GCD, DispatchQueue
is a queue object that manages the execution of tasks on your app's main
thread or on a background thread
.
It is a FIFO 3 queue to which your application can submit tasks in the form of block objects (functions or closures).
DispatchQueues
:
- maintain a queue of tasks and execute these tasks, either serially or concurrently, in their turn.
- hide all thread management related activities. (You can configure a queue, but you won’t interact directly with any thread associated with it.)
- are thread safe: They can be accessed from different threads simultaneously without locking. (Developers can use
DispatchQueues
to make their own code thread safe.)
Work submitted to dispatch queues
executes on the pool of threads managed by the system.
Except for the dispatch queue (the main queue
) representing your app's main thread
, the system makes no guarantees about which thread it uses to execute a task.
Thread Pools Thread creation and destruction are expensive processes.
Instead of creating a new thread whenever a task is to be executed, then destroying it when the task finishes, available threads are taken from a pool of available threads created and managed by the operating system (Thread Pool pattern).
A sample thread pool (green boxes) with waiting tasks (blue) and completed tasks (yellow)
Source: https://en.wikipedia.org/wiki/Thread_pool
Tasks
Tasks in GCD:
- encapsulate code and data into a single object.
- are lightweight, easy to create, and easy to enqueue.
- can be expressed either as a function or as an anonymous "block" of code (eg, a closure).
3 FIFO: First In, First Out — Tasks run in the order in which they are added to the queue — the first task in the queue will be the first to start. Though each block of code will be started in the order they were submitted, because more than one code block can be executed at the same time, the order in which tasks finish isn't guaranteed.
Tasks placed into a queue can either run synchronously or asynchronously.
Synchronous — Submits a task for execution on the current queue and returns control to the calling function only after that code block (task) finishes executing.
When you schedule a work item (task) synchronously, your app will wait and block the current thread's run loop until execution of the current task finishes, before returning control to the current queue and executing the next task.
Diagram 1 - Synchronous Task, Same Queue
In this diagram, each task must wait for the preceding task to complete before it will be executed. Once started, each task will prevent (block) any other code from accessing the current queue (the main queue
, in this case) until done:
Diagram 2 - Synchronous Task, Different Queue
Again, each task must also wait for the preceding task to complete before it will be executed. Once started, each task will still block the current queue until completed, even though the submitted task executes on a different queue.
If the current queue is the main queue
, then this will block any UI-related tasks from proceeding until the submitted tasks is completed on the other queue's thread.
Source: https://medium.com/shakuro/introduction-to-ios-concurrency-a5db1cf18fa6
Asynchronous — Schedules a task for immediate execution, and immediately returns control to the calling function.
When you schedule a work item (task) asynchronously, that task:
- will be submitted to its queue immediately, but it will also return execution to your app immediately, ordering the submitted task to be executed but not waiting for it. This way, the app is free to run other tasks while the submitted task is executing.
- can be submitted by code on one thread but actually run on a different thread. This allows the task to be started immediately and to take advantage of additional processor resources to finish their work more quickly.
An asynchronous task (a closure or function) does not block the current thread of execution from proceeding on to the next function, and your code on the current thread does not wait for the submitted task to finish — it continues executing while the submitted task runs elsewhere.
The way you work with threads in GCD is by creating a DispatchQueue
.
When you create a DispatchQueue
, the OS will potentially create and assign one or more threads to the queue. If existing threads are available in the pool, they can be reused; if not, then the OS will create them as needed.
It's easy to create a DispatchQueue
. This example creates a new DispatchQueue
called myQueue
with a label (identifier) of "com.makeschool.mycoolapp.networking"
:
let myQueue = DispatchQueue(label: "com.makeschool.mycoolapp.networking")
The label:
argument needs to be a unique identifier. The example above illustrates the preferred practice of using a reverse-DNS name (eg, com.your_company.your_app) to guarantee uniqueness (you could also use a UUID).
And because the label:
helps immensely when debugging, it is a good idea to assign it text that is meaningful to you (ie, the ".networking" token above).
To define whether a task runs synchronously or asynchronously, you call the .sync
or the .async
function on your newly-created queue:
let myQueue = DispatchQueue(label: "com.makeschool.mycoolapp.networking")
myQueue.sync {
// do something synchronous here...
}
Required Resources:
TODO:
- run the
GCDPlay.playground
and observe its output
Q: What can you infer about the order in which the 2 for loops
execute?
Q: On what queue does the following for loop
run?
for i in 100..<110 {
print("🐳 ", i)
}
- Currently, the playground runs the
.sync
function on the queue labeled"com.makeschool.queue"
- change
.sync
to.async
and run the playground again
Q: How has the output changed after changing .sync
to .async
? (ie, in what order do the for loops
execute now?)
GCDPlay.playground code:
import Foundation
let queue = DispatchQueue(label: "com.makeschool.queue")
queue.sync {
for i in 0..<10 {
print("🍎 ", i)
}
}
for i in 100..<110 {
print("🐳 ", i)
}
Share/discuss diagrammed solutions...
- Research:
DispatchObject
DispatchWorkItem
dispatchMain()
- Complete reading
- Complete challenges