Slide 1

Slide 1 text

Simple Concurrent Programming Chris Eidhof

Slide 2

Slide 2 text

First, a story.

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

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

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

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

Slide 18

Slide 18 text

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

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

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

Slide 21

Slide 21 text

Problems and solutions

Slide 22

Slide 22 text

Problem Your scrolling is too slow

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

What's happening? Shared resource: CPU

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

UIKit is not thread-safe.

Slide 27

Slide 27 text

1. Take drawRect: code and isolate it into an operation 2. Replace original view by UIImageView 3. Update UIImageView on main thread Async drawing

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Problem Loading an image over the network

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 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 Aside GCD

Slide 35

Slide 35 text

What if the request is blocking the queue?

Slide 36

Slide 36 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 37

Slide 37 text

Built on top of Grand Central Dispatch Operation Queues

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

NSOperation* downloadOperation = [DownloadOperation downloadOperationWithURL:url]; downloadOperation.completionBlock = ^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.imageView.image = image; }]; }; Step 4: Custom NSOperation subclass

Slide 42

Slide 42 text

NSURLSession* session = [NSURLSession sharedSession]; [session downloadTaskWithURL:url completionHandler: ^(NSURL *location, NSURLResponse *response, NSError *err) { // Process downloaded data. }]; (or use NSURLSession)

Slide 43

Slide 43 text

Problem You want to import a (large) data set

Slide 44

Slide 44 text

Normal Core Data Stack

Slide 45

Slide 45 text

NSManagedObjectContext NSManagedObjectContext NSManagedObjectContext SQLite NSPersistentStore NSPersistentStoreCoordinator Double MOC Stack

Slide 46

Slide 46 text

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

Slide 47

Slide 47 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 48

Slide 48 text

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

Slide 49

Slide 49 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 50

Slide 50 text

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

Slide 51

Slide 51 text

What about fetched results controllers?

Slide 52

Slide 52 text

No content

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

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

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

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

+ (id)sharedInstance { static id sharedInstance = nil; @synchronized(self) { if (sharedInstance == nil) { sharedInstance = [[self alloc] init]; } } return sharedInstance; } Singletons

Slide 66

Slide 66 text

+ (id)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); } Faster than synchronized, but still blocking.

Slide 67

Slide 67 text

• Lock contention • Starvation • Priority Inversion Other Problems

Slide 68

Slide 68 text

• Lots of subtle problems that don't show up during testing. • Keep it very simple • Use the main thread when possible Conclusion

Slide 69

Slide 69 text

... one more thing

Slide 70

Slide 70 text

Pasta Theory of Software

Slide 71

Slide 71 text

Spaghetti Code

Slide 72

Slide 72 text

Lasagna Code

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

[email protected] • @chriseidhof • http://www.objc.io • http://www.uikonf.com Thanks

Slide 75

Slide 75 text

Resources

Slide 76

Slide 76 text

• 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

Slide 77

Slide 77 text

• WWDC12 #211: Concurrent User Interfaces on iOS