Upload
istanbul-tech-talks
View
121
Download
7
Embed Size (px)
Citation preview
Practical Concurrent ProgrammingChris Eidhof
Merhabā
Why is concurrency hard?
When to use concurrency?
Always use the main thread
When to use concurrency?
— Networking
— Expensive stuff
Decision workflow
1. Measure
2. Change
3. Measure again
How to draw things in the background
Recipe
1. Take drawRect: code
2. Put it in a background thread
3. Update the main thread
The drawRect: code
- (void)drawRect:(CGRect)rect{ CGContextRef ctx = UIGraphicsGetCurrentContext(); // expensive // drawing // code}
Moving it to a different thread
[queue addOperationWithBlock:^{ UIGraphicsBeginImageContextWithOptions(size, NO, 0); // expensive // drawing // code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // TODO: update the main thread}];
Updating the main thread
[queue addOperationWithBlock:^{ UIGraphicsBeginImageContextWithOptions(size, NO, 0); // expensive // drawing // code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = i; }];}];
Deckset filters
[self.queue addOperationWithBlock:^{ CIImage *ciImage = [CIImage imageWithContentsOfURL:sourceURL]; CIFilter *depthOfFieldFilter = [CIFilter filterWithName:@"CIDepthOfField"]; ... CIImage *finalImage = [alphaFilter valueForKey:kCIOutputImageKey]; CGContextRef cgContext = CGBitmapContextCreate(NULL, size.width, size.height, 8, size.width * 4, colorSpace, kCGImageAlphaPremultipliedLast); CIContext *context = [CIContext contextWithCGContext:cgContext options:nil]; CGImageRef outputImage = [context createCGImage:finalImage fromRect:ciImage.extent]; ... CGImageDestinationAddImage(destination, outputImage, nil); CGImageDestinationFinalize(destination);}];
... the shared resource was the GPU!
Solution
NSDictionary *options = @{kCIContextUseSoftwareRenderer: @YES};CIContext *context = [CIContext contextWithCGContext:cgContext options:options];
... and ...
self.queue.maxConcurrentOperationCount = 1;
Solution, part 2
... we removed the complicated filter
How to not load things from the network
dispatch_async(backgroundQueue, ^{ NSData *data = [NSData dataWithContentsOfURL:url]; NSArray *graphItems = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; UIImage* image = [self drawGraph:graphItems]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; });});
Grand Central Dispatch
Threading is hardGCD moves it to the system-level
GCD Thread PoolMain Thread
High Priority Queue
SerialQueue
ParallelQueue
Serial Queue
MainQueue
SerialQueue
ConcurrentQueue
SerialQueue
Default Priority Queue
Low Priority Queue
Background Priority Queue
Custom Queues
GCD Queues
Threads
— Simpler
— Faster
— Thread-pool management
— Memory-efficient
— Async means: no deadlock!
How to load things from the network
Use NSURLSession
Operation Queues
dispatch_async(backgroundQueue, ^{ NSArray *graphData = [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; });});
Step 1: Use NSOperation
NSOperationQueue* drawingQueue = [[NSOperationQueue alloc] init];[drawingQueue addOperationWithBlock:^{ NSArray *graphData = [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }];}];
How to cancel this?
Step 2: Use NSBlockOperation
NSBlockOperation* drawingOperation = [NSBlockOperation blockOperationWithBlock:^{ NSArray *graphData = [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; }];[drawingQueue addOperation:drawingOperation];
Step 3: Pull out the completion handler
NSOperation* drawingOperation = [NSBlockOperation blockOperationWithBlock:^{ NSArray *graphData = [self generateDataPointsForGraph]; self.image = [self drawGraph:graphData];}];drawingOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = self.image; }];};
Step 4: Custom NSOperation subclass
NSData *data = [self generateDataPointsForGraph];NSOperation* drawingOperation = [DrawingOperation drawingOperationWithData:data];drawingOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }];};
How to efficiently import data into Core Data
Efficiently Importing Data
— Implement Find-or-Create Efficiently
— Reduce Peak Memory Footprint
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html
Importing employees
for (WebserviceEmployee *webserviceEmployee) { NSNumber *identifier = webserviceEmployee.identifier; Employee *employee = [self findEmployeeWithIdentifier:identifier]; if (employee == nil) { employee = [self createEmployeWithIdentifier:identifier]; } // Update data}
Importing more efficiently
NSMutableArray *identifiers = [NSMutableArray array];for (WebserviceEmployee *webserviceEmployee) { NSNumber *identifier = webserviceEmployee.identifier; [identifiers addObject:identifier];}NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(employeeID IN %@)", identifiers];
Import in batchesUse a separate context
Normal Core Data Stack
NSManagedObjectContextNSManagedObjectContext NSManagedObjectContext
SQLite
NSPersistentStore
NSPersistentStoreCoordinator
Double MOC Stack
Small imports
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];context.persistentStoreCoordinator = self.ptStoreCoordinator;[self.context performBlock:^{ [self import];}];
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note){ NSManagedObjectContext *moc = self.mainMOC; if (note.object != moc) { [moc performBlock:^(){ [moc mergeChangesFromContextDidSaveNotification:note]; }]; };}];
NSManagedObjectContextNSManagedObjectContext NSManagedObjectContext!
SQLite
NSPersistentStore
NSPersistentStoreCoordinator!
!
!
Double MOC Stack
You might want to consider using a different concurrency style, and this time you have two persistent store coordinators, two almost completely separate Core Data stacks.
Source: http://asciiwwdc.com/2013/sessions/211
SQLite!
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext!
!
!NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext!
!
!
SQLite!
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext!
!
!NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext
NSPersistentStore
NSPersistentStoreCoordinator
NSManagedObjectContext!
!
!
NSManagedObjectContextNSManagedObjectContext NSManagedObjectContext!
SQLite
NSPersistentStore
NSPersistentStoreCoordinator!
!
!
Importing Recap
Didn't turn out to be a problem: we shipped the sqlite file for the initial import.
How to make objects play well when concurrent
@interface Account : NSObject
@property (nonatomic) double balance;
- (void)transfer:(double)euros to:(Account*)other;
@end
- (void)transfer:(double)euros to:(Account*)other{ self.balance = self.balance - euros; other.balance = other.balance + euros;}
What happens if two methods call this method at the same time? From different threads?
The same code, how the compiler sees it
- (void)transfer:(double)euros to:(Account*)other{ double currentBalance = self.balance; self.balance = currentBalance - euros; double otherBalance = other.balance; other.balance = otherBalance + euros;}
a b [a.transfer:20 to:b] [a.transfer:30 to:b]
100 0
currentBalance = 100
currentBalance = 100
100 0
a.balance = 100 - 20
80 0
b.balance = b.balance + 20
80 20
a.balance = 100 - 30
70 20
a b [a.transfer:20 to:b] [a.transfer:30 to:b]
100 0
currentBalance = 100
currentBalance = 100
100 0
a.balance = 100 - 20
80 0
b.balance = b.balance + 20
80 20
a.balance = 100 - 30
70 20
- (void)transfer:(double)euros to:(Account*)other{ @synchronized(self) { self.balance = self.balance - euros; other.balance = other.balance + euros; }}
- (void)transfer:(double)euros to:(Account*)other{ @synchronized(self) { @synchronized(other) { self.balance = self.balance - euros; other.balance = other.balance + euros; }
}}
Problem: deadlock.
- (void)transfer:(double)euros to:(Account*)other{ @synchronized(self.class) { self.balance = self.balance - euros; other.balance = other.balance + euros; }}
Solution: move concurrency to a different level
Do it the GCD way
Account* account = [Account new];Account* other = [Account new];dispatch_queue_t accountOperations = dispatch_queue_create("accounting", DISPATCH_QUEUE_SERIAL);dispatch_async(accountOperations, ^{ [account transfer:200 to:other];});
dispatch_async will never block.
There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first
method is far more difficult.
— Tony Hoare
Thanks
— @chriseidhof
— http://www.objc.io
— http://www.uikonf.com
— http://www.decksetapp.com
Resources
— Concurrency Programming Guide
— Threading Programming Guide
— NSOperationQueue class reference
— http://www.objc.io/issue-2/
— http://www.opensource.apple.com/source/objc4/objc4-551.1/runtime/objc-sync.mm
— http://googlemac.blogspot.de/2006/10/synchronized-swimming.html
— WWDC12 #211: Concurrent User Interfaces on iOS
Icons are from the Noun Project:
— Coffee Maker by Maureen Placente
— Coffee by Julia Soderberg
— Railroad Crossing by Edward Boatman
— Database by Stefan Parnarov
— Drawing by Daniel Shannon
— Hammer by Alex AS
— Lock by P.J. Onori
— Photoshop by Joe Harrison
— Register by Wilson Joseph