Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Practical Asynchronous Programming - Kiev

Practical Asynchronous Programming - Kiev

Transcript

  1. Practical Asynchronous Programming Chris Eidhof

  2. Привіт!

  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. It's all about shared resources ☞ GPU ☞ CPU ☞

    Areas in memory (objects, numbers, ...) ☞ Files
  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. Why is concurrency hard? It's hard to test asynchronous apps,

    because it's non-deterministic
  18. Why is concurrency hard? Lots of tricky problems (more on

    that later)
  19. ❝Before you even consider redesigning your code to support concurrency,

    you should ask yourself whether doing so is necessary.❞ — Concurrency Programming Guide
  20. Recipe Improving the responsiveness of your app ➀ Determine what

    is slow: analyze and measure ➁ If neccessary: factor out units of work ➂ Isolate the units in a separate queue ➃ Measure again
  21. Problem Your scrolling is too slow

  22. Draw Complicated Drawing Core data URL Request Draw What's happening?

    (The main thread)
  23. What's happening? Shared resource: CPU

  24. How to solve this? We should free up the main

    thread so that drawing and user input gets handled immediately
  25. UIKit is not thread-safe.

  26. Async drawing ➀ Take drawRect: code and isolate it into

    an operation ➁ Replace original view by UIImageView ➂ Update UIImageView on main thread
  27. Before: - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // expensive

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

    // code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = i; }]; }];
  29. Async CoreImage [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); }]; What's the problem?
  30. NSDictionary *options = @{kCIContextUseSoftwareRenderer: @NO}; CIContext *context = [CIContext contextWithCGContext:cgContext

    options:options]; ... and ... self.queue.maxConcurrentOperationCount = 1;
  31. Problem Drawing a complicated image

  32. Problem Drawing a complicated image dispatch_async(backgroundQueue, ^{ NSArray *graphData =

    [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); });
  33. Problem Drawing a complicated image dispatch_async(backgroundQueue, ^{ NSArray *graphData =

    [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); }); How to remove this from the queue?
  34. Problem Drawing a complicated image dispatch_async(backgroundQueue, ^{ NSArray *graphData =

    [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); }); How to cancel this if the operation already started?
  35. Operation Queues Built on top of Grand Central Dispatch

  36. Step 1: Use NSOperation NSOperationQueue* drawingQueue; [drawingQueue addOperationWithBlock:^{ NSArray *graphData

    = [self generateDataPointsForGraph]; UIImage* image = [self drawGraph:graphData]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; }]; How to cancel this?
  37. 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];
  38. 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; }]; };
  39. Step 4: Custom NSOperation subclass NSOperation* drawingOperation = [DrawingOperation drawingOperationWithData:[self

    generateDataPointsForGraph]]; drawingOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; };
  40. Let's add some networking 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; }); });
  41. 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
  42. What if the request is blocking the queue?

  43. 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.
  44. Problem You want to import a large data set into

    core data
  45. Normal Core Data Stack

  46. NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext SQLite NSPersistentStore NSPersistentStoreCoordinator Double MOC Stack

  47. Small imports NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; context.persistentStoreCoordinator =

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

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

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

    ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! !
  52. SQLite ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! !

    ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! ! NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext ! SQLite NSPersistentStore NSPersistentStoreCoordinator ! ! !
  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. 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; }
  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. 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.
  64. Realize: at which level in your app do you want

    to be concurrent?
  65. Other Problems ☞ Lock contention ☞ Starvation ☞ Priority Inversion

  66. Value objects

  67. A Person object @interface Person : NSObject @property (nonatomic,copy) NSString*

    name; @property (nonatomic) NSDate* birthDate; @property (nonatomic) NSUInteger numberOfKids; @end
  68. Mutation Sometimes mutation is very handy. Most of the times

    it will bite you.
  69. @interface Person : NSObject @property (nonatomic,readonly) NSString* name; @property (nonatomic,readonly)

    NSDate* birthDate; @property (nonatomic,readonly) NSUInteger numberOfKids; - (instancetype) initWithName:(NSString*)name birthDate:(NSDate*)birthDate numberOfKids:(NSUInteger)numberOfKids; @end
  70. Value Objects ☞ Use lots of them. ☞ Make them

    immutable, and use the immutable collections.
  71. KVO self.queue = [NSOperationQueue new]; [self addObserver:self forKeyPath:@"name" options:0 context:nil];

    self.name = @"hello"; [self.queue addOperationWithBlock:^{ self.name = @"hi"; }];
  72. KVO Observing - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

    { NSLog(@"is main thread: %d", [NSThread isMainThread]); }
  73. KVO Results 2014-03-16 14:29:45.563 AsyncProgramming[71243:60b] is main thread: 1 2014-03-16

    14:29:45.566 AsyncProgramming[71243:1303] is main thread: 0
  74. Designing an Async API ☞ How do you communicate back?

    Delegates? Blocks? ☞ On which threads are the callbacks? ☞ At which level are you concurrent?
  75. Conclusion ☞ Lots of subtle problems that don't show up

    during testing. ☞ Make it easy for yourself ☞ Use the main thread when possible
  76. UIKonf

  77. Дякую ☞ @chriseidhof ☞ http://www.objc.io ☞ http://www.uikonf.com ☞ http://www.decksetapp.com

  78. 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
  79. ☞ WWDC12 #211: Concurrent User Interfaces on iOS