Multithreading on iOS
-
Upload
make-school -
Category
Software
-
view
1.165 -
download
0
Transcript of Multithreading on iOS
WHY DO WE NEED THREADS?
Listen for User InputRespond to User Input
Application Code
Time
Listen for User InputRespond to User Input
Application Code
Listen for User InputRespond to User Input
Application Code
…
MULTITHREADING BASICS
The entire program is blocked while one piece of code is running!
@IBAction func downloadButtonPressed(sender: AnyObject) { downloadData() updateBusyIndicator() }
updateBusyIndicator:downloadData:
downloadButton
Pressed:
Thread 1 (Main Thread)
MULTITHREADING BASICS
Long running tasks block our program. The UI
freezes!
Threads allow us to run multiple tasks in parallel!
MULTITHREADING BASICS
By using a background thread we can unblock the UI thread!
updateBusyIndicator:
downloadButtonTapped:
Thread 1 (Main Thread)
Thread 2 (Background Thread)
downloadData:
updateBusyIndicator:
updateBusyIndicator:
updateBusyIndicator:
WHY DO WE NEED THREADS?
Listen for User InputRespond to User Input
Application Code
Time
Listen for User InputRespond to User Input
Application Code
Listen for User InputRespond to User Input
Application Code
…
MULTITHREADING BASICS
Listen for User InputRespond to User Input
Application CodeTime
Listen for User InputRespond to User Input
Application Code
…
}Long-runningtask
Application Code
Listen for User InputRespond to User Input
Application Code
Main Thread Background Thread
MULTITHREADING BASICS
Multithreading works independently of the underlying
hardware architecture
Multiple threads can run on a single core, each thread gets
a certain amount of execution time
MULTITHREADING BASICS
Spawning and executing a thread and switching between threads consumes resources, especially on a
single core system multithreading hurts overall
performance (even though it can improve perceived
performance through responsiveness)
In most UI Frameworks (including UIKit) UI updates are only
possible from the main thread
WHEN TO USE MULTITHREADING
Improve responsiveness of application by performing long
running tasks in background threads
Improve Performance by utilizing multiple cores at once
CLOSURESAnonymous functions that can capture and modify
variables of their surrounding context
Are reference types; can be passed around in code
MULTITHREADING ON IOS
You can create a new Thread by creating an instance of
NSThread (not recommended)
Most of the time you should be using GCD (Grand Central
Dispatch)
GCD
Abstraction for the concept of threads, instead we think of
different queues for our application
GCD determines which queues are mapped to which
threads
Closure based API
GCD MINDSET
Instead of thinking about particular threads, think whether
work should happen on main or background thread
Think about whether certain tasks should be performed
concurrently or sequentially
GCD
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { let data = downloadData() dispatch_async(dispatch_get_main_queue()) { updateUI(data) }}
GCD
dispatch_sync blocks the current thread until the block is
executed
dispatch_async let’s the current thread continue, executes
the dispatched block some time in the future
SYNCHRONIZATION OF THREADSParallel execution of code leads to situations where multiple
threads access the same resources at (almost) the same time
In many cases this is undesirable:
One thread reads an instance of NSMutableArray while another
thread manipulates the same instance (unpredictable result)
Some operations in our applications need to be performed
atomically
SYNCHRONIZATION OF THREADS
Balance:1000$
1
2
3
-100$
-900$
-200$
Balance:-200$
1b
2b
3b
Account Balance UpdateAccount Transaction
Balance:1000$ 1
-100$
1b
Balance:900$ 2
-900$
2b
Balance:0$ 3
-200$
} One atomictransaction
SYNCHRONIZATION OF THREADSMost commonly you will use a mutex lock
(mutual exclusion lock) to synchronize code
With a mutex lock you can lock a piece of
code to only be run by one thread at a time@synchronized(self)
Thre
ad A
@synchronized(self)
Thre
ad B
Thre
ad C
Thre
ad D
@synchronized(self) { if ( (self.balance - amount) >= 0) { self.balance -= amount; } }
Obj-C Code - Swift lacks this simple API:
DEADLOCKSDeadlocks can occur when a thread is waiting for an
event which cannot happen anymore
These situations can occur when more than one lock is
involved
Example:
Thread A has acquired Lock “objectA” and is trying to
acquire additional Lock “objectB”
Thread B has acquired Lock “objectB” and is trying to
acquire additional Lock “objectA”
Try to avoid acquiring multiple locks
@synchronized(objectA)
Thre
ad A
@synchronized(objectB)
@synchronized(objectB)
@synchronized(objectA)
@synchronized(objectB)
Thre
ad B
@synchronized(objectA)
@synchronized(objectA)
@synchronized(objectB)
RACE CONDITIONS
Race Conditions are bugs that only occur when multiple threads
access shared resources in a specific order
For this reason Race Conditions are very hard to debug
Critical shared resources should be protected by synchronization
Debugging threading issues can become almost impossible - good design upfront is extremely important
LOCKSSwift does not (yet) have a mutex lock API like Obj-C has
Developers came up with their own API:
// API Definitionfunc synced(lock: AnyObject, closure: () -> ()) { objc_sync_enter(lock) closure() objc_sync_exit(lock)}
// Usagesynced(self) { println("This is a synchronized closure")}
Source: http://stackoverflow.com/questions/24045895/what-is-the-swift-equivalent-to-objective-cs-synchronized
Usually using a serialqueue is better than using locks!
SERIAL DISPATCH QUEUES
Preferred over locks - has better performance and conveys intention clearer
Serial queue guarantees that all dispatched blocks get executed after each
other - only one running at a time
class Account { var balance = 0 let accessBankAccountQueue = dispatch_queue_create("com.makeschool.bankaccount", nil) func withdraw(amount: Int) { dispatch_sync(accessBankAccountQueue) { if ( (self.balance - amount) >= 0) { self.balance -= amount; } } } }
NSOPERATIONQUEUEMore abstract version of GCD Queue, based on Objective-C objects
(NSOperations) instead of blocks
Provides additional features:
Dependencies between operations
Operation Priorities
Operations can be cancelled
Always performs operations concurrently while respecting dependencies
NSOPERATIONQUEUElet downloadQueue = NSOperationQueue()
let downloadOperation = NSBlockOperation { // perform download here print("downloading")}
downloadOperation.queuePriority = .Low
let updateDBOperation = NSBlockOperation { // update DB here print("update DB")}
// update DB after download completesupdateDBOperation.addDependency(downloadOperation)
// add operations to queue to get them starteddownloadQueue.addOperations([downloadOperation, updateDBOperation], waitUntilFinished: false)
DISPATCH SEMAPHOREDispatch semaphores are a flexible way to block one thread until another
thread sends a signal
Also useful to restrict access to finite resources (provide count > 0 in semaphore initializer), called counting semaphore
let currentOperationSemaphore = dispatch_semaphore_create(0)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)) { print("work") dispatch_semaphore_signal(currentOperationSemaphore)}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)) { dispatch_semaphore_wait(currentOperationSemaphore, DISPATCH_TIME_FOREVER) print("done")}
DISPATCH GROUPSWhen you want to create a group of actions and wait until all actions in
that group are completed you can use dispatch groupslet mainQueueGroup = dispatch_group_create()
for (var i = 0; i < 10; i++) { dispatch_group_async(mainQueueGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { [i] in print("Download Data: \(i)") }}
dispatch_group_wait(mainQueueGroup, DISPATCH_TIME_FOREVER)print("All Downloads Completed")
dispatch_group_wait blocks the current thread until all operations in the dispatch group are
completed
Alternatively you can define a block that shall be called when the group completes by using
dispatch_group_notify
DISPATCH GROUPSlet mainQueueGroup = dispatch_group_create()
for (var i = 0; i < 10; i++) { dispatch_group_async(mainQueueGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) { [i] in print("Download Data: \(i)") }}
dispatch_group_wait(mainQueueGroup, DISPATCH_TIME_FOREVER)print("All Downloads Completed")
Example Output: Download Data: 2Download Data: 0Download Data: 3Download Data: 1Download Data: 4Download Data: 5Download Data: 6Download Data: 7Download Data: 8Download Data: 9All Downloads Completed
SIDE NOTE: ASYNCHRONOUS CODE IN PLAYGROUNDS
import Cocoaimport XCPlayground
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)) { print("happens later")}
XCPSetExecutionShouldContinueIndefinitely()
Source: http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground
SUMMARYMostly we use multithreaded code to create non-blocking UIs by executing
long running tasks on a background thread
GCD that uses blocks is our favorite way to implement multithreading on
iOS because it allows the OS to choose the most efficient implementation
Multithreaded code needs to be designed well to avoid race conditions
and deadlocks and to group certain tasks into transactions
Our favorite tools to implement well designed multithreaded code are the
serial dispatch queue and the NSOperationQueue
EXERCISE
Download the Starter Project.
1. Create a non-blocking version of the app that updates the progress label correctly
using GCD
2. Improve the performance of the app by submitting each operation as an individual
block, ensure that the “Completed” message is still displayed correctly
3. Refactor the code to use GCD groups to determine when all tasks have completed
4. Increase the operation count from 20 to 1000 and add buttons to start and cancel
operations (hint: use NSOperationQueue)
REFERENCES
Apple Docs: Swift Closures
Erica Sadun: Capturing References in Closures
Apple Docs: Migrating Away From Threads
Apple Docs: GCD Reference
Blog Post: More than you want to know about @synchronized