Pragma Mark: Simple Concurrent Programming

Pragma Mark: Simple Concurrent Programming

Ade0c334ecff1448bb96f5f733bf1f83?s=128

Chris Eidhof | @chriseidhof

October 26, 2013
Tweet

Transcript

  1. Simple Concurrent Programming Chris Eidhof

  2. First, a story.

  3. [self performSelector:@selector(update) withObject:nil afterDelay:0]

  4. dispatch_async(dispatch_get_main_queue(), ^{ [self update]; });

  5. 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.
  6. • Your scrolling is too slow • When doing network

    requests, your app gets unresponsive • When importing data, your app gets unresponsive
  7. • GPU • CPU • Areas in memory (objects, numbers,

    ...) • Files It's all about shared resources
  8. Why is concurrency hard?

  9. 1. Write code

  10. 2. ⌘-R

  11. 3. Test on your device: everything works

  12. Better: TDD

  13. 1. Write Unit Test

  14. 2. Write Code

  15. 3. ⌘-U: All tests succeeded

  16. Problem You might still have bugs.

  17. It's hard to test asynchronous apps, because it's non- deterministic

    Why is concurrency hard?
  18. Lots of tricky problems (more on that later) Why is

    concurrency hard?
  19. Before you even consider redesigning your code to support concurrency,

    you should ask yourself whether doing so is necessary. — Concurrency Programming Guide
  20. 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
  21. Problems and solutions

  22. Problem Your scrolling is too slow

  23. Draw Complicated Drawing Core data URL Request Draw (The main

    thread) What's happening?
  24. What's happening? Shared resource: CPU

  25. We should free up the main thread so that drawing

    and user input gets handled immediately How to solve this?
  26. UIKit is not thread-safe.

  27. 1. Take drawRect: code and isolate it into an operation

    2. Replace original view by UIImageView 3. Update UIImageView on main thread Async drawing
  28. - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // expensive //

    drawing // code } Before:
  29. [queue addOperationWithBlock:^{ UIGraphicsBeginImageContextWithOptions(size, NO, 0); // expensive // drawing //

    code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = i; }]; }]; After:
  30. Problem Loading an image over the network

  31. 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
  32. 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
  33. 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
  34. 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
  35. What if the request is blocking the queue?

  36. 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.
  37. Built on top of Grand Central Dispatch Operation Queues

  38. 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
  39. 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
  40. 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
  41. NSOperation* downloadOperation = [DownloadOperation downloadOperationWithURL:url]; downloadOperation.completionBlock = ^{ [[NSOperationQueue mainQueue]

    addOperationWithBlock:^{ self.imageView.image = image; }]; }; Step 4: Custom NSOperation subclass
  42. NSURLSession* session = [NSURLSession sharedSession]; [session downloadTaskWithURL:url completionHandler: ^(NSURL *location,

    NSURLResponse *response, NSError *err) { // Process downloaded data. }]; (or use NSURLSession)
  43. Problem You want to import a (large) data set

  44. Normal Core Data Stack

  45. NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext SQLite NSPersistentStore NSPersistentStoreCoordinator Double MOC Stack

  46. NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; context.persistentStoreCoordinator = self.ptStoreCoordinator; [self.context

    [[performBlock:^{]] [self import]; }]; Small imports
  47. [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) { NSManagedObjectContext *moc

    = self.mainMOC; if (note.object != moc) { [moc performBlock:^(){ [moc mergeChangesFromContextDidSaveNotification:note]; }]; }; }];
  48. NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext ! SQLite NSPersistentStore NSPersistentStoreCoordinator ! ! !

    Double MOC Stack
  49. 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
  50. SQLite ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! !

    ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! !
  51. What about fetched results controllers?

  52. None
  53. General concurrency problems Locking

  54. @interface Account : NSObject @property (nonatomic) double balance; - (void)transfer:(double)euros

    to:(Account*)other; @end
  55. - (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?
  56. - (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
  57. 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
  58. 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
  59. - (void)transfer:(double)euros to:(Account*)other { @synchronized(self) { self.balance = self.balance -

    euros; other.balance = other.balance + euros; } }
  60. - (void)transfer:(double)euros to:(Account*)other { @synchronized(self) { @synchronized(other) { self.balance =

    self.balance - euros; other.balance = other.balance + euros; } } } Problem: deadlock.
  61. - (void)transfer:(double)euros to:(Account*)other { @synchronized(self.class) { self.balance = self.balance -

    euros; other.balance = other.balance + euros; } } Working, but possibly slow
  62. - (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 ... } }
  63. 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
  64. Realize: at which level in your app do you want

    to be concurrent?
  65. + (id)sharedInstance { static id sharedInstance = nil; @synchronized(self) {

    if (sharedInstance == nil) { sharedInstance = [[self alloc] init]; } } return sharedInstance; } Singletons
  66. + (id)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance =

    [[self alloc] init]; }); } Faster than synchronized, but still blocking.
  67. • Lock contention • Starvation • Priority Inversion Other Problems

  68. • Lots of subtle problems that don't show up during

    testing. • Keep it very simple • Use the main thread when possible Conclusion
  69. ... one more thing

  70. Pasta Theory of Software

  71. Spaghetti Code

  72. Lasagna Code

  73. 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
  74. • chris@eidhof.nl • @chriseidhof • http://www.objc.io • http://www.uikonf.com Thanks

  75. Resources

  76. • 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
  77. • WWDC12 #211: Concurrent User Interfaces on iOS