Share
Embed
Pragma Mark: Simple Concurrent Programming
Chris Eidhof | @chriseidhof
October 26, 2013
Transcript
77 slides
-
double delayInSeconds = 0.1; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds *
NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // make sure this gets triggered after a layout pass. }); Rethink your code. -
• Your scrolling is too slow • When doing network
requests, your app gets unresponsive • When importing data, your app gets unresponsive -
• GPU • CPU • Areas in memory (objects, numbers,
...) • Files It's all about shared resources -
It's hard to test asynchronous apps, because it's non- deterministic
Why is concurrency hard? -
Lots of tricky problems (more on that later) Why is
concurrency hard? -
Before you even consider redesigning your code to support concurrency,
you should ask yourself whether doing so is necessary. — Concurrency Programming Guide -
1. Determine what is slow: analyze and measure 2. If
neccessary: factor out units of work 3. Isolate the units in a separate queue 4. Measure again Recipe Improving the responsiveness of your app -
Draw Complicated Drawing Core data URL Request Draw (The main
thread) What's happening? -
We should free up the main thread so that drawing
and user input gets handled immediately How to solve this? -
1. Take drawRect: code and isolate it into an operation
2. Replace original view by UIImageView 3. Update UIImageView on main thread Async drawing -
- (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // expensive //
drawing // code } Before: -
[queue addOperationWithBlock:^{ UIGraphicsBeginImageContextWithOptions(size, NO, 0); // expensive // drawing //
code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = i; }]; }]; After: -
dispatch_async(backgroundQueue, ^{ NSData* imageData = [NSData dataWithContentsOfURL:url]; UIImage* image =
[[UIImage alloc] initWithData:imageData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView = image; }); }); Warning: please don't use this code Problem Loading an image over the network -
dispatch_async(backgroundQueue, ^{ NSData* imageData = [NSData dataWithContentsOfURL:url]; UIImage* image =
[[UIImage alloc] initWithData:imageData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView = image; }); }); How to cancel this? Problem Loading an image over the network -
dispatch_async(backgroundQueue, ^{ NSData* imageData = [NSData dataWithContentsOfURL:url]; UIImage* image =
[[UIImage alloc] initWithData:imageData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView = image; }); }); What happens if the request times out? Problem Loading an image over the network -
GCD Thread Pool Main Thread High Priority Queue Serial Queue
Parallel Queue Serial Queue Main Queue Serial Queue Concurrent Queue Serial Queue Default Priority Queue Low Priority Queue Background Priority Queue Custom Queues GCD Queues Threads Aside GCD -
GCD Thread Pool Main Thread High Priority Queue Serial Queue
Parallel Queue Serial Queue Main Queue Serial Queue Concurrent Queue Serial Queue Default Priority Queue Low Priority Queue Background Priority Queue Custom Queues GCD Queues Threads GCD adds more threads. This is expensive. -
NSOperationQueue* downloadQueue; [downloadQueue addOperationWithBlock:^{ NSData* imageData = [NSData dataWithContentsOfURL:url]; UIImage*
image = [UIImage imageWithData:imageData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; }]; How to cancel this? Step 1: Use NSOperation -
NSBlockOperation* downloadOperation = [NSBlockOperation blockOperationWithBlock:^{ NSData* imageData = [NSData dataWithContentsOfURL:url];
UIImage* image = [UIImage imageWithData:imageData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; }]; [downloadQueue addOperation:downloadOperation]; // Sometime later [downloadOperation cancel]; Step 2: Use NSBlockOperation -
NSOperation* downloadOperation = [NSBlockOperation blockOperationWithBlock:^{ NSData* imageData = [NSData dataWithContentsOfURL:url];
self.downloadedImage = [UIImage imageWithData:imageData]; }]; downloadOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = self.downloadedImage; }]; }; Step 3: Pull out the completion handler -
NSOperation* downloadOperation = [DownloadOperation downloadOperationWithURL:url]; downloadOperation.completionBlock = ^{ [[NSOperationQueue mainQueue]
addOperationWithBlock:^{ self.imageView.image = image; }]; }; Step 4: Custom NSOperation subclass -
NSURLSession* session = [NSURLSession sharedSession]; [session downloadTaskWithURL:url completionHandler: ^(NSURL *location,
NSURLResponse *response, NSError *err) { // Process downloaded data. }]; (or use NSURLSession) -
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; context.persistentStoreCoordinator = self.ptStoreCoordinator; [self.context
[[performBlock:^{]] [self import]; }]; Small imports -
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) { NSManagedObjectContext *moc
= self.mainMOC; if (note.object != moc) { [moc performBlock:^(){ [moc mergeChangesFromContextDidSaveNotification:note]; }]; }; }]; -
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 ! ! ! -
@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? -
- (void)transfer:(double)euros to:(Account*)other { double currentBalance = self.balance; self.balance =
currentBalance - euros; double otherBalance = other.balance; other.balance = otherBalance + euros; } The same code, how the compiler sees it -
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; } } Working, but possibly slow -
- (void)transfer:(double)euros to:(Account*)other { objc_sync_enter(self.class) objc_exception_try_enter setjmp objc_exception_extract self.balance =
self.balance - euros; other.balance = other.balance + euros; objc_exception_try_exit objc_sync_exit(self.class) ... objc_exception_throw ... } } -
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. Do it the GCD way -
Realize: at which level in your app do you want
to be concurrent? -
+ (id)sharedInstance { static id sharedInstance = nil; @synchronized(self) {
if (sharedInstance == nil) { sharedInstance = [[self alloc] init]; } } return sharedInstance; } Singletons -
+ (id)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance =
[[self alloc] init]; }); } Faster than synchronized, but still blocking. -
• Lots of subtle problems that don't show up during
testing. • Keep it very simple • Use the main thread when possible Conclusion -
The ideal software structure is one having components that are
small and loosely coupled; this ideal structure is called ravioli code. In ravioli code, each of the components, or objects, is a package containing some meat or other nourishment for the system; any component can be modified or replaced without significantly affecting other components. — http://www.gnu.org/fun/jokes/pasta.code.html Ravioli Code -
• Concurrency Programming Guide • http://googlemac.blogspot.de/2006/10/ synchronized-swimming.html • NSOperationQueue class
reference • http://www.objc.io/issue-2/ • http://www.gnu.org/fun/jokes/pasta.code.html • https://developer.apple.com/library/ios/ documentation/Cocoa/Conceptual/Multithreading/ ThreadSafetySummary/ • http://www.opensource.apple.com/source/objc4/ objc4-551.1/runtime/objc-sync.mm