THREADS, QUEUES, AND MORE: ASYNC PROGRAMMING IN IOS Why Async?
Long running is: >= 0.01666 seconds
Perform long running tasks asynchronously Threads vs Queues
QueueThread QueueThreads
Main Queue Main Queue UI { code… } UI UI { code… } { … } Main Queue manages the main thread
Main Queue UI { code… } UI UI { code… } { … } User Interface / UIKit all happens on the main queue Main Queue UI { code… } UI UI { code… } { … } Any custom code in view controllers (for example, viewDidLoad) executes on main queue
Main Queue How do we get off the main queue?
NSObject / NSThread Grand Central Dispatch NSOperation Networking NSObject / NSThread
NSObject methods - performSelector:withObject:afterDelay: - performSelector:withObject:afterDelay:inModes: - performSelectorOnMainThread:withObject:waitUntilDone: - performSelectorOnMainThread:withObject:waitUntilDone:modes: - performSelector:onThread:withObject:waitUntilDone: - performSelector:onThread:withObject:waitUntilDone:modes: - performSelectorInBackground:withObject: …this approach uses threads, not queues. NOT recommended.
Grand Central Dispatch Grand Central Dispatch • Dispatch Queues • Global • Concurrent • Serial • Dispatch Groups
Dispatch Queues First, get a reference to the queue
Dispatch Async - Swift 3 let queue = DispatchQueue.global(qos: .background) for iteration in 1...iterations { queue.async { [weak self] in self?.longRunningTask(with: iteration) } } Dispatch Queue Types - Swift 3 let queue:DispatchQueue switch approach { case .dispatchConcurrent: queue = DispatchQueue.global(qos: .default) case .dispatchSerial: queue = DispatchQueue(label: "SerialQueue") default: queue = DispatchQueue(label: "ConcurrentQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) }
Add work to the queue Dispatch Async - Swift 3 let queue = DispatchQueue.global(qos: .background) for iteration in 1...iterations { queue.async { [weak self] in self?.longRunningTask(with: iteration) } }
Dispatch Work Item - Swift 3 let workItem = DispatchWorkItem(qos: .default, flags: .assignCurrentContext) { … } queue.async(execute: workItem) Go back to the main queue to update UI
Dispatch Async - Swift 3 … DispatchQueue.main.async { strongSelf.taskDelegate?.longRunningTaskDidComplete(with: results) } How does it look?
Group tasks First, create a group
Dispatch Group - Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Enter the group
Dispatch Group - Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Do your work
Dispatch Group - Swift 3 func longRunningTask(with iteration:Int) { var iterationResults:[String] = [] for counter in 1...10 { Thread.sleep(forTimeInterval: 0.1) iterationResults.append("Iteration (iteration) - Item (counter)") } if self.approach == .dispatchGroup { self.results.append(contentsOf: iterationResults) } else { self.resultsQueue.sync { self.results.append(contentsOf: iterationResults) } } } Leave the group
Dispatch Group - Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Notify - group is “done”
Dispatch Group - Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } How does it look?
Broken! What’s wrong?
Arrays are *not* thread safe Try with sync approach
Dispatch Group - Swift 3 func longRunningTask(with iteration:Int) { var iterationResults:[String] = [] for counter in 1...10 { Thread.sleep(forTimeInterval: 0.1) iterationResults.append("Iteration (iteration) - Item (counter)") } if self.approach == .dispatchGroup { self.results.append(contentsOf: iterationResults) } else { self.resultsQueue.sync { self.results.append(contentsOf: iterationResults) } } } Better?
OperationQueue
Create an OperationQueue OperationQueue - Swift 3 var taskOperationQueue:OperationQueue = OperationQueue() // for a serial queue, do this: taskOperationQueue.maxConcurrentOperationCount = 1
Add an Operation to the Queue OperationQueue - Swift 3 for iteration in 1...iterations { taskOperationQueue.addOperation({ [weak self] in self?.longRunningTask(with: iteration) }) }
OperationQueue - Swift 3 var operationsToAdd:[Operation] = [] var previousOperation:Operation? for iteration in 1...iterations { let newOperation = CustomOperation(iteration: iteration, delegate: taskDelegate) if let actualPreviousOperation = previousOperation { newOperation.addDependency(actualPreviousOperation) } operationsToAdd.append(newOperation) previousOperation = newOperation } taskOperationQueue.addOperations(operationsToAdd, waitUntilFinished: false) Go back to the main queue to update UI
OperationQueue - Swift 3 OperationQueue.main.addOperation { strongSelf.taskDelegate?.longRunningTaskDidComplete(with: results) } In action…
OperationQueue • InvocationOperation, BlockOperation, or Operation subclass • OperationQueues and Operations support cancellation • OperationQueues and Operations support dependencies, even across queues • Can suspend and resume a queue; clear all operations out of a queue Networking
Use URLSession & URLSessionTasks Networking - Swift 3 let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 5 config.httpAdditionalHeaders = ["X-Token":"F06427CB-00AE-422C-992F-854689B5419E"] self.urlSession = URLSession(configuration: config)
Networking - Swift 3 var iterationComponents:URLComponents = URLComponents() iterationComponents.scheme = "http" iterationComponents.host = "joes-macbook-air.local" iterationComponents.port = 8080 iterationComponents.path = "/iteration(iteration).json" guard let iterationURL = iterationComponents.url else { return nil } let request = URLRequest(url: iterationURL) let task = urlSession?.dataTask(with: request, completionHandler: { (data, response, error) in … } NOTE: “error” means cannot connect to host Response may also be an error. Check response status code too.
Networking - Swift 3 if let actualError = error { print("Error encountered with data task: (actualError.localizedDescription)") return } guard let actualResponse = response as? HTTPURLResponse, actualResponse.statusCode == 200 else { print("Unexpected response received") return } Completion handler is on background queue - do your “heavy lifting” there
Networking - Swift 3 guard let actualData = data else { print("No data received...") return } let json = try? JSONSerialization.jsonObject(with: actualData, options: []) guard let info = json as? [String:Any], let results = info["iterations"] as? [String] else { print("Data received was not in the expected format") return } Dispatch to main queue to update UI with results
Networking - Swift 3 DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: results) } Things to Avoid
General Async Risks Simultaneous Updates
Race Conditions / Out of Sequence Updates Deadlocks
Thread Explosion Updating UI from background thread
Unexpected queue from block-based SDK method
Unexpected Queue In iOS SDK, watch out for: • Completion Handlers • Error Handlers • any method parameter titled “withBlock:” Over-dispatching
Over-dispatching dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(workQueue, ^{ [[self managedObjectContext] performBlock:^{ [testObject setValue:@"test" forKey:@"testKey"]; }]; }); Retain Cycles
Retain Cycles • Happens when the caller retains the block or closure, and the block or closure retains the caller. • Different semantics for preventing in Objective-C, Swift 3.0 • Gist is: Avoid a strong reference to the caller from the block / closure unless absolutely necessary, and make sure the reference gets released Learn more? MARTIANCRAFT TRAINING…
@jwkeeley THANK YOU martiancraft.com

Threads, Queues, and More: Async Programming in iOS

  • 1.
    THREADS, QUEUES, AND MORE:ASYNC PROGRAMMING IN IOS Why Async?
  • 2.
    Long running is: >=0.01666 seconds
  • 3.
    Perform long runningtasks asynchronously Threads vs Queues
  • 4.
  • 5.
    Main Queue Main Queue UI { code… } UIUI { code… } { … } Main Queue manages the main thread
  • 6.
    Main Queue UI { code… } UI UI { code… } { … } UserInterface / UIKit all happens on the main queue Main Queue UI { code… } UI UI { code… } { … } Any custom code in view controllers (for example, viewDidLoad) executes on main queue
  • 7.
    Main Queue How dowe get off the main queue?
  • 8.
    NSObject / NSThread GrandCentral Dispatch NSOperation Networking NSObject / NSThread
  • 9.
    NSObject methods - performSelector:withObject:afterDelay: -performSelector:withObject:afterDelay:inModes: - performSelectorOnMainThread:withObject:waitUntilDone: - performSelectorOnMainThread:withObject:waitUntilDone:modes: - performSelector:onThread:withObject:waitUntilDone: - performSelector:onThread:withObject:waitUntilDone:modes: - performSelectorInBackground:withObject: …this approach uses threads, not queues. NOT recommended.
  • 10.
    Grand Central Dispatch GrandCentral Dispatch • Dispatch Queues • Global • Concurrent • Serial • Dispatch Groups
  • 11.
    Dispatch Queues First, geta reference to the queue
  • 12.
    Dispatch Async -Swift 3 let queue = DispatchQueue.global(qos: .background) for iteration in 1...iterations { queue.async { [weak self] in self?.longRunningTask(with: iteration) } } Dispatch Queue Types - Swift 3 let queue:DispatchQueue switch approach { case .dispatchConcurrent: queue = DispatchQueue.global(qos: .default) case .dispatchSerial: queue = DispatchQueue(label: "SerialQueue") default: queue = DispatchQueue(label: "ConcurrentQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) }
  • 13.
    Add work to thequeue Dispatch Async - Swift 3 let queue = DispatchQueue.global(qos: .background) for iteration in 1...iterations { queue.async { [weak self] in self?.longRunningTask(with: iteration) } }
  • 14.
    Dispatch Work Item- Swift 3 let workItem = DispatchWorkItem(qos: .default, flags: .assignCurrentContext) { … } queue.async(execute: workItem) Go back to the main queue to update UI
  • 15.
    Dispatch Async -Swift 3 … DispatchQueue.main.async { strongSelf.taskDelegate?.longRunningTaskDidComplete(with: results) } How does it look?
  • 17.
  • 18.
    Dispatch Group -Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Enter the group
  • 19.
    Dispatch Group -Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Do your work
  • 20.
    Dispatch Group -Swift 3 func longRunningTask(with iteration:Int) { var iterationResults:[String] = [] for counter in 1...10 { Thread.sleep(forTimeInterval: 0.1) iterationResults.append("Iteration (iteration) - Item (counter)") } if self.approach == .dispatchGroup { self.results.append(contentsOf: iterationResults) } else { self.resultsQueue.sync { self.results.append(contentsOf: iterationResults) } } } Leave the group
  • 21.
    Dispatch Group -Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } Notify - group is “done”
  • 22.
    Dispatch Group -Swift 3 func performLongRunningTask(with iterations:Int) { let queue:DispatchQueue = DispatchQueue.global(qos: .default) let workGroup = DispatchGroup() for iteration in 1...iterations { print("In iteration (iteration)") queue.async { [weak self] in workGroup.enter() self?.longRunningTask(with: iteration) workGroup.leave() } } workGroup.notify(queue: queue) { DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: self.results) print("Called delegate for group") } } } How does it look?
  • 23.
  • 24.
    Arrays are *not* threadsafe Try with sync approach
  • 25.
    Dispatch Group -Swift 3 func longRunningTask(with iteration:Int) { var iterationResults:[String] = [] for counter in 1...10 { Thread.sleep(forTimeInterval: 0.1) iterationResults.append("Iteration (iteration) - Item (counter)") } if self.approach == .dispatchGroup { self.results.append(contentsOf: iterationResults) } else { self.resultsQueue.sync { self.results.append(contentsOf: iterationResults) } } } Better?
  • 26.
  • 27.
    Create an OperationQueue OperationQueue- Swift 3 var taskOperationQueue:OperationQueue = OperationQueue() // for a serial queue, do this: taskOperationQueue.maxConcurrentOperationCount = 1
  • 28.
    Add an Operation tothe Queue OperationQueue - Swift 3 for iteration in 1...iterations { taskOperationQueue.addOperation({ [weak self] in self?.longRunningTask(with: iteration) }) }
  • 29.
    OperationQueue - Swift3 var operationsToAdd:[Operation] = [] var previousOperation:Operation? for iteration in 1...iterations { let newOperation = CustomOperation(iteration: iteration, delegate: taskDelegate) if let actualPreviousOperation = previousOperation { newOperation.addDependency(actualPreviousOperation) } operationsToAdd.append(newOperation) previousOperation = newOperation } taskOperationQueue.addOperations(operationsToAdd, waitUntilFinished: false) Go back to the main queue to update UI
  • 30.
    OperationQueue - Swift3 OperationQueue.main.addOperation { strongSelf.taskDelegate?.longRunningTaskDidComplete(with: results) } In action…
  • 32.
    OperationQueue • InvocationOperation, BlockOperation,or Operation subclass • OperationQueues and Operations support cancellation • OperationQueues and Operations support dependencies, even across queues • Can suspend and resume a queue; clear all operations out of a queue Networking
  • 33.
    Use URLSession & URLSessionTasks Networking- Swift 3 let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 5 config.httpAdditionalHeaders = ["X-Token":"F06427CB-00AE-422C-992F-854689B5419E"] self.urlSession = URLSession(configuration: config)
  • 34.
    Networking - Swift3 var iterationComponents:URLComponents = URLComponents() iterationComponents.scheme = "http" iterationComponents.host = "joes-macbook-air.local" iterationComponents.port = 8080 iterationComponents.path = "/iteration(iteration).json" guard let iterationURL = iterationComponents.url else { return nil } let request = URLRequest(url: iterationURL) let task = urlSession?.dataTask(with: request, completionHandler: { (data, response, error) in … } NOTE: “error” means cannot connect to host Response may also be an error. Check response status code too.
  • 35.
    Networking - Swift3 if let actualError = error { print("Error encountered with data task: (actualError.localizedDescription)") return } guard let actualResponse = response as? HTTPURLResponse, actualResponse.statusCode == 200 else { print("Unexpected response received") return } Completion handler is on background queue - do your “heavy lifting” there
  • 36.
    Networking - Swift3 guard let actualData = data else { print("No data received...") return } let json = try? JSONSerialization.jsonObject(with: actualData, options: []) guard let info = json as? [String:Any], let results = info["iterations"] as? [String] else { print("Data received was not in the expected format") return } Dispatch to main queue to update UI with results
  • 37.
    Networking - Swift3 DispatchQueue.main.async { self.taskDelegate?.longRunningTaskDidComplete(with: results) } Things to Avoid
  • 38.
  • 39.
    Race Conditions / Outof Sequence Updates Deadlocks
  • 40.
    Thread Explosion Updating UIfrom background thread
  • 41.
  • 42.
    Unexpected Queue In iOSSDK, watch out for: • Completion Handlers • Error Handlers • any method parameter titled “withBlock:” Over-dispatching
  • 43.
    Over-dispatching dispatch_queue_t workQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0); dispatch_async(workQueue, ^{ [[self managedObjectContext] performBlock:^{ [testObject setValue:@"test" forKey:@"testKey"]; }]; }); Retain Cycles
  • 44.
    Retain Cycles • Happenswhen the caller retains the block or closure, and the block or closure retains the caller. • Different semantics for preventing in Objective-C, Swift 3.0 • Gist is: Avoid a strong reference to the caller from the block / closure unless absolutely necessary, and make sure the reference gets released Learn more? MARTIANCRAFT TRAINING…
  • 45.