Slide 1

Slide 1 text

Practical Asynchronous Programming Chris Eidhof

Slide 2

Slide 2 text

Привіт!

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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.

Slide 6

Slide 6 text

☞ Your scrolling is too slow ☞ When doing network requests, your app gets unresponsive ☞ When importing data, your app gets unresponsive

Slide 7

Slide 7 text

It's all about shared resources ☞ GPU ☞ CPU ☞ Areas in memory (objects, numbers, ...) ☞ Files

Slide 8

Slide 8 text

Why is concurrency hard?

Slide 9

Slide 9 text

1. Write code

Slide 10

Slide 10 text

2. ⌘-R

Slide 11

Slide 11 text

3. Test on your device: everything works

Slide 12

Slide 12 text

Better: TDD

Slide 13

Slide 13 text

1. Write Unit Test

Slide 14

Slide 14 text

2. Write Code

Slide 15

Slide 15 text

3. ⌘-U: All tests succeeded

Slide 16

Slide 16 text

Problem You might still have bugs.

Slide 17

Slide 17 text

Why is concurrency hard? It's hard to test asynchronous apps, because it's non-deterministic

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

❝Before you even consider redesigning your code to support concurrency, you should ask yourself whether doing so is necessary.❞ — Concurrency Programming Guide

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Problem Your scrolling is too slow

Slide 22

Slide 22 text

Draw Complicated Drawing Core data URL Request Draw What's happening? (The main thread)

Slide 23

Slide 23 text

What's happening? Shared resource: CPU

Slide 24

Slide 24 text

How to solve this? We should free up the main thread so that drawing and user input gets handled immediately

Slide 25

Slide 25 text

UIKit is not thread-safe.

Slide 26

Slide 26 text

Async drawing ➀ Take drawRect: code and isolate it into an operation ➁ Replace original view by UIImageView ➂ Update UIImageView on main thread

Slide 27

Slide 27 text

Before: - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); // expensive // drawing // code }

Slide 28

Slide 28 text

After: [queue addOperationWithBlock:^{ UIGraphicsBeginImageContextWithOptions(size, NO, 0); // expensive // drawing // code UIImage *i = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = i; }]; }];

Slide 29

Slide 29 text

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?

Slide 30

Slide 30 text

NSDictionary *options = @{kCIContextUseSoftwareRenderer: @NO}; CIContext *context = [CIContext contextWithCGContext:cgContext options:options]; ... and ... self.queue.maxConcurrentOperationCount = 1;

Slide 31

Slide 31 text

Problem Drawing a complicated image

Slide 32

Slide 32 text

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; }); });

Slide 33

Slide 33 text

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?

Slide 34

Slide 34 text

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?

Slide 35

Slide 35 text

Operation Queues Built on top of Grand Central Dispatch

Slide 36

Slide 36 text

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?

Slide 37

Slide 37 text

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];

Slide 38

Slide 38 text

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; }]; };

Slide 39

Slide 39 text

Step 4: Custom NSOperation subclass NSOperation* drawingOperation = [DrawingOperation drawingOperationWithData:[self generateDataPointsForGraph]]; drawingOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; };

Slide 40

Slide 40 text

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; }); });

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

What if the request is blocking the queue?

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

Problem You want to import a large data set into core data

Slide 45

Slide 45 text

Normal Core Data Stack

Slide 46

Slide 46 text

NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext SQLite NSPersistentStore NSPersistentStoreCoordinator Double MOC Stack

Slide 47

Slide 47 text

Small imports NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; context.persistentStoreCoordinator = self.ptStoreCoordinator; [self.context performBlock:^{ [self import]; }];

Slide 48

Slide 48 text

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) { NSManagedObjectContext *moc = self.mainMOC; if (note.object != moc) { [moc performBlock:^(){ [moc mergeChangesFromContextDidSaveNotification:note]; }]; }; }];

Slide 49

Slide 49 text

NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext ! SQLite NSPersistentStore NSPersistentStoreCoordinator ! ! ! Double MOC Stack

Slide 50

Slide 50 text

❝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

Slide 51

Slide 51 text

SQLite ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! !

Slide 52

Slide 52 text

SQLite ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! ! NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext NSPersistentStore NSPersistentStoreCoordinator NSManagedObjectContext ! ! ! NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext ! SQLite NSPersistentStore NSPersistentStoreCoordinator ! ! !

Slide 53

Slide 53 text

General concurrency problems Locking

Slide 54

Slide 54 text

@interface Account : NSObject @property (nonatomic) double balance; - (void)transfer:(double)euros to:(Account*)other; @end

Slide 55

Slide 55 text

- (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?

Slide 56

Slide 56 text

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; }

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

- (void)transfer:(double)euros to:(Account*)other { @synchronized(self.class) { self.balance = self.balance - euros; other.balance = other.balance + euros; } } Working, but possibly slow

Slide 62

Slide 62 text

- (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 ... } }

Slide 63

Slide 63 text

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.

Slide 64

Slide 64 text

Realize: at which level in your app do you want to be concurrent?

Slide 65

Slide 65 text

Other Problems ☞ Lock contention ☞ Starvation ☞ Priority Inversion

Slide 66

Slide 66 text

Value objects

Slide 67

Slide 67 text

A Person object @interface Person : NSObject @property (nonatomic,copy) NSString* name; @property (nonatomic) NSDate* birthDate; @property (nonatomic) NSUInteger numberOfKids; @end

Slide 68

Slide 68 text

Mutation Sometimes mutation is very handy. Most of the times it will bite you.

Slide 69

Slide 69 text

@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

Slide 70

Slide 70 text

Value Objects ☞ Use lots of them. ☞ Make them immutable, and use the immutable collections.

Slide 71

Slide 71 text

KVO self.queue = [NSOperationQueue new]; [self addObserver:self forKeyPath:@"name" options:0 context:nil]; self.name = @"hello"; [self.queue addOperationWithBlock:^{ self.name = @"hi"; }];

Slide 72

Slide 72 text

KVO Observing - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"is main thread: %d", [NSThread isMainThread]); }

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Designing an Async API ☞ How do you communicate back? Delegates? Blocks? ☞ On which threads are the callbacks? ☞ At which level are you concurrent?

Slide 75

Slide 75 text

Conclusion ☞ Lots of subtle problems that don't show up during testing. ☞ Make it easy for yourself ☞ Use the main thread when possible

Slide 76

Slide 76 text

UIKonf

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

☞ WWDC12 #211: Concurrent User Interfaces on iOS