π 2019.112.18 (WED)
WWDC 2016 | Session : 720 | Category : Performance
π
Concurrent Programming With GCD in Swift 3 - WWDC 2016 - Videos - Apple Developer
Concurrency allows multiple parts of your application to run at the same time. On our system. you achieve concurrency by creating threads.
A CPU core can execute one of your threads at any given time, but the payoff for introducing concurrency, the penalty for introducing concurrency to your application is it's much more difficult to maintain your thread safety.
The other threads that you've introduced can observe the effects of you breaking your code invariants while you're performing operations on other threads.
β GCD
multi-threaded code that works on everything from an Apple Watch trough all of our iOS devices.
A dispatch queue is a construct that allows you to submit items of work to that queue. In Swift, this is closures. And dispatch will bring up a thread and service that work for you.
When dispatch is finished running all of the work on that thread, it can tear that worker thread down for you itself.
You can create your own thread and on those threads, you might run Run loops.
And then finally, on the first slide we saw you get your main thread, and the main thread is special. It gets both a main run loop and a main queue.
So dispatch queues have two main ways you can submit work for them, the first of which is asynchronous execution.
This is where you can queue up multiple items of work to your dispatch queue and then dispatch will bring up a thread to execute that work.
Dispatch will one by one take items off that queue and execute them. And then when finished all items on the queue the system will reclaim the thread that it bought up for you.
This is where, if we have the same setup as we had before, the dispatch queue with some asynchrounous work.
let queue = DispatchQueue(label: "com.example.imagetransform")
queue.async {
let smallImage = image.resize(to: rect)
DispatchQueue.main.async {
imageView.image = smallImage
}
}
- Create a Dispatch Queue to whick you submit work
- Dispatch Queues execute work items in FIFO order
- Use
.async
to execute your work on the queue - Dispatch main queue executes all items on the main thread
- Simple to chain work between queues
You have to contorl a concurrency in you appliation.
The thread pool that dispath uses will limit the concurrency you achieve in order to use all of the callls in you device.
If the user interface spawns off three separate work items here, you can create a dispatch group.
Use synchroize execution to hepl you serialize state between subsystems.
Serial queues, dispatch queues are seral by natural.
-
Can use subsystem serail queues for mutual exclusion
-
Use
.sync
to safely access properties form usbsystems -
Be aware of "lock ordering: introduced between subtems
var count: Int { queue.sync { self.connections.count } }
-
Use
.async
to subimt work with a specific QoS class -
Dipatch helps reslove priority inversions
-
Create single-purpose queuses with a specific QoS class
queue.async(qos: .background) { print("Maintenance work") }
queue.async(qos: .userInitiated) { print("Button tapped") }
-
By default
.async
captures execution context at time of submission -
Create
DispatchWorkItem
from closures to control execution properties -
Use
.assignCurrentContext
to capture current QoS at time of creationlet item = DipatchworkItem(flage: .assignCurrentContext) { pritn("Hello WWDC 2016!") }
queue.async(execute: item)
- Use
.wait
on work items to signal that this item needs to excute - Dispatch elevates priority of queued work ahead
- Waiting with a
DispatchWorkItem
gives ownership information - Semaphores and Groups do not admit a concept of ownership
15
Use DispatchQueue.sync(execute:)
-
harder to misuse than traiditional locks, more robust
-
better instrumentaion (Xcode assertions...)
// Use Explicit Synchronization
class MyObject { private let internalState: Int private let internalQueue: DispatchQueue var state: Int { get { return internalQueue.sync { internalState } } set (newState) { internalQueue.sync { internalState = newState } } } }
-Avoid data corruption
GCD lets you express several precoditions
- Code is running on a given queue
- Code is not running on a given queue
dispatchPrecondition(.onQueue(expectedQueue)))
dispatchPrecondition(.not
All these subsystems have a reference in these objects, and getting rid of them actually can be a challenge.
- Set up is about creating the object and giving it the property you need it to have for its purpose.
- You actually make this object be known to other subsystems. You start using it in a more concurrent world in performance duties.
- Invalidation is about making sure that all the parts, all your subsystems know that this object is going away.
- Deallocation
- Setup
picking the properties that you want for your code, and the animation, and all that.
class BusyController: SubsystemObserving {
init(...) { ... }
- Activation
start receiving these state notifications, state changes notifications, so we will register with that subsystem, and ask for notifications to be received on the main queue.
class BusyController: SubsystemObserving {
init(...) { ... }
func activate() {
DataTransform.sharedInsance.register(observer: self, queue: DispatchQueue.main)
}
}
- Delloation
BusyController is reference from the main queue and your user interface.
However, when we register it with the ata transform subsystems, it was very likely that reference was taken from the data structure onto this object, which means that deinit doesn't run, which means hat it get unregisterd, it doesn't get collected, and you end up with abandoned mememory.
BusyController should be managed from the main thread, and you want to make sure that users of your API do that properly. So you will want to have a precondition that this only happens ont the main thread, or the main queue even.
You want to track invalidation, for example, as a Boolean in your object, and you remeber when you did. At the same time, let's throw in more preconditions, and make sure, enforce, that before your object is deallocated, it has been properly invalidated. It will help you find bugs.
β¬οΈ
In your code that handles the notification for the state transitions, we an observe that the object was invalidated and actually drop the notification on the floor and update the UI in a way that wold be open.
Setup for dispatch objects is all the things you can do when you build the object and all the attricutes you can pass.
handler β that will run when the resource that your monitoring fires, and that is events pending.
It used to be that for dispatch sources, the initial resume had exactly that meaning. We've actually now made supension and activiation be two seperate concepts. Also in resume activate can be called several times, and it only acts once. The contract however is that once you've called activate, you won't mutate the properties of your objects anymore.
Cancellation in sources does the thing that you'd expect, which is that you stop getting events for the thing you're monitoring.
Organize your application around data flows into independent subsystems
Synchronize state with Dispatch Queues
Use the activate/invalidate pattern