Chapter 4. Concurrent Programming
iOS devices have two or three CPU cores (see Table 3-1). This means, even if the main thread (the UI thread) is busy updating the screen, the app can still be doing more computations in the background without the need for any context switch.
In this chapter, we explore various options for making the best use of the available CPU cores, and we’ll learn how to optimize performance using concurrent programming. We will discuss the following topics:
-
Creating and managing threads
-
The Great Central Dispatch (GCD) abstraction
-
Operations and queues
We will cover best practices and techniques for writing thread-safe, highly performant code.
Threads
A thread is a sequence of instructions that can be executed by a runtime.
Each process has at least one thread. In iOS, the primary thread on which the process is started is commonly referred to as the main thread. This is the thread in which all UI elements are created and managed. All interrupts related to user interaction are ultimately dispatched to the UI thread where the handler code is written—your IBAction
methods are all executed in the main thread.
Cocoa programming does not allow updating UI elements from other threads. This means that whenever the app executes background threads for long operations such as network or other processing, the code must perform a context switch to the main thread to update the UI—for example, the progress bar indicating the task progress or the label indicating the outcome of the process.
The Cost of Threads
However great it may look to have several threads in the app, each thread has a cost associated with it that impacts app performance. Each thread not only takes some time during creation but also uses up memory in the kernel as well as the app’s memory space.1
Stack Size
The main thread stack size is 1 MB and cannot be changed. Any secondary thread is allocated 512 KB of stack space by default. Note that the full stack is not immediately created. The actual stack size grows with use. So, even if the main thread has a stack size of 1 MB, at some point in time, the actual stack size may be much smaller.
Before a thread starts, the stack size can be changed. The minimum allowed stack size is 16 KB, and the size must be a multiple of 4 KB. The sample code in Example 4-1 shows how you can configure the stack size before starting a thread.
Example 4-1. Change thread stack size
+(
NSThread
*
)
createThreadWithTarget:
(
id
)
target
selector:
(
SEL
)
selector
object:
(
id
)
argument
stackSize:
(
NSUInteger
)
size
{
if
(
(
size
%
4096
)
!=
0
)
{
return
nil
;
}
NSThread
*
t
=
[[
NSThread
alloc
]
initWithTarget
:
target
selector
:
selector
object
:
argument
];
t
.
stackSize
=
size
;
return
t
;
}
Creation Time
A quick test on an iPhone 6 Plus running iOS 8.4 showed average thread creation time (not including the start time) ranged between 4,000–5,000 µs, which is about 4–5 ms.
The time taken to actually start a thread after creation ranged from anywhere between 5 ms to well over 100 ms, averaging about 29 ms. That can be a lot of time, especially if you start multiple threads during app launch.
The elongated time for thread start can be attributed to several context switches that have overheads.
For brevity, the code for these computations has been omitted here. For details, see the computeThreadCreationTime
method in the code on GitHub. Figure 4-1 shows the output from that code.
GCD
The Grand Central Dispatch (GCD) API is comprised of core language features, runtime libraries, and system enhancements for concurrent code execution.
We will not get into the fundamentals of using GCD, as that is not the purpose of this book. You most likely already have a fair background working with GCD constructs, but if you need a review of GCD fundamentals, check out Ray Wenderlich’s “Multithreading and Great Central Dispatch on iOS for Beginners Tutorial”.
However, for completeness, we will run through a quick list of what GCD provides:
-
Task or dispatch queues, which allow execution on the main thread, concurrent execution, and serial execution
-
Dispatch groups, which allow tracking execution of a group of tasks, irrespective of the underlying queue they are submitted on
-
Semaphores
-
Barriers, which allow creating synchronization points in a concurrent dispatch queue
-
Dispatch object and source management, which allow low-level management and monitoring
GCD handles thread creation and management well. It also helps you to keep the total number of threads in your app under control and not cause any leaks.
Caution
While most apps will generally perform well using GCD alone, there are specific cases when you should consider using NSThread
or NSOperationQueue
. In the scenarios where your app has multiple long-running tasks to be executed concurrently, it is better to take control of the thread creation. If your code takes longer to complete, you may soon hit the limit of 64,2,3 the maximum GCD thread pool size.
Be wary about using dispatch_async
and dispatch_sync
lavishly too, as it can lead to app crashes.4 Although 64 threads might look like a reasonably high number for a mobile app, the app may hit the limit sooner than later.
Operations and Queues
The next set of abstractions available for managing tasks in iOS programming is operations and operation queues.
NSOperation
encapsulates a task and its associated data and code, whereas NSOperationQueue
controls execution of one or more of such tasks in a FIFO order.
NSOperation
and NSOperationQueue
both provide control over the number of threads that get created. You control the number of queues formed. You also control the number of threads in each queue, using the maxConcurrentOperationCount
property.
These two options sit somewhere in between using NSThread
(where it is left to the developer to manage all concurrency) and GCD (where the OS manages concurrency).
Here’s a quick comparison of the NSThread
, NSOperationQueue
, and GCD APIs:
- GCD
-
-
Highest abstraction.
-
Two queues are available out of the box:
main
andglobal
. -
Can create more queues (using
dispatch_queue_create
). -
Can request exclusive access (using
dispatch_barrier_sync
anddispatch_barrier_async
). -
Manages underlying threads.
-
Hard limit on 64 threads created.
-
NSOperationQueue
-
-
No default queues.
-
App manages the queues it creates.
-
Queues are priority queues.
-
Operations can have different priorities (use the
queuePriority
property). -
Operations can be cancelled using the
cancel
message. Note thatcancel
is merely a flag. If an operation is under execution, it may continue to execute. -
Can wait for an operation to complete (use the
waitUntilFinished
message).
-
NSThread
-
-
Lowest-level construct, gives maximum control.
-
App creates and manages threads.
-
App creates and manages thread pools.
-
App starts the threads.
-
Threads can have priority. OS uses this for scheduling their execution.
-
No direct API to wait for a thread to complete. Use a mutex (e.g.,
NSLock
) and custom code.
-
Thread-Safe Code
All throughout our software engineering lives, we are told to always write thread-safe code—meaning that if multiple threads execute the same instruction sets concurrently, there should not be any negative side effects.
There are two broad techniques for achieving this:
-
If you cannot avoid using a modifiable shared state, make your code thread-safe.
These techniques are easier said than done. There are a number of choices available to accomplish them.
Because an app will have a modifiable shared state, we need to establish best practices for application state management and modifications.
One basic rule that drives these best practices is “Preserve invariants in the code.”5
Atomic Properties
Atomic properties are a great start to making your application state thread-safe. If a property is atomic
, the modification or retrieval is guaranteed to be atomic.
This is important because it prevents two threads from simultaneously updating a value, which otherwise could result in a corrupted state. The thread that is modifying the property must complete before the other thread can proceed.
All properties are atomic by default. As a best practice, use atomic
explicitly where this is appropriate. To mark a property otherwise, use the nonatomic
attribute. Example 4-2 demonstrates both atomic and nonatomic properties.
Example 4-2. Atomic and nonatomic properties
@property
(
atomic
)
NSString
*
firstName
;
@property
(
nonatomic
)
NSString
*
department
;
Because atomic properties have overheads, it is advisable not to overuse them. For example, when it can be guaranteed that a property will never be accessed from more than one thread at any time, it is better to mark it nonatomic
.
One such scenario is working with IBOutlet
s. @property (nonatomic, readwrite, strong) IBOutlet UILabel *nameLabel
should be preferred over @property (atomic, readwrite, strong) IBOutlet UILabel *nameLabel
because we know that UIKit allows manipulating UI elements from only the main thread. Because access will be in one designated thread, marking the property atomic
will only add overhead without bringing any value.
Synchronized Blocks
Even if the properties are marked atomic
, the eventual code using them may not be thread-safe. An atomic property only prohibits concurrent modification. Assuming that we have an entity HPUser
that can be updated using an operation HPOperation
, let’s have a look at Example 4-3.
Example 4-3. Using atomic properties across threads
//An entity (partial definition)
@interface
HPUser
@property
(
atomic
,
copy
)
NSString
*
firstName
;
@property
(
atomic
,
copy
)
NSString
*
lastName
;
@end
//A service class (declaration omitted for brevity)
@implementation
HPUpdaterService
-(
void
)
updateUser:
(
HPUser
*
)
user
properties:
(
NSDictionary
*
)
properties
{
NSString
*
fn
=
[
properties
objectForKey
:
@"firstName"
];
if
(
fn
!=
nil
)
{
user
.
firstName
=
fn
;
}
NSString
*
ln
=
[
properties
objectForKey
:
@"lastName"
];
if
(
ln
!=
nil
)
{
user
.
lastName
=
ln
;
}
}
@end
Let’s consider that the updateUser:properties:
method is called whenever the user pulls down to refresh and data is available from the server. It may also be called by a sync task that executes periodically.
So, at some point in time, there is a possibility that multiple responses will attempt to update the user profile concurrently—maybe on two cores or just using time-slicing.
Consider the scenario where two responses in different threads try to update the user with the names “Bob Taylor” and “Alice Darji.” Without atomic updates on the properties firstName
and lastName
, the order of execution is not guaranteed and the final result can be any combination, including “Alice Taylor” and “Bob Darji.”
This example is only demonstrative but enforces the point that atomic properties are not enough to make code thread-safe.
This brings us to the next best practice: all related state updates should be batched in a single transaction.
Use the @synchronized
directive to create a mutex and enter a critical section, which can only be executed by one thread at any point in time. The code may be updated as shown in Example 4-4.
Example 4-4. Thread-safe blocks
@implementation
HPUpdaterService
-
(
void
)
updateUser:
(
HPUser
*
)
user
properties:
(
NSDictionary
*
)
properties
{
@synchronized
(
user
)
{
NSString
*
fn
=
[
properties
objectForKey
:
@"
firstName
"
]
;
if
(
fn
!
=
nil
)
{
user
.
firstName
=
fn
;
}
NSString
*
ln
=
[
properties
objectForKey
:
@"
lastName
"
]
;
if
(
ln
!
=
nil
)
{
user
.
lastName
=
ln
;
}
}
}
@end
With this change, the final name of the user will be either “Bob Taylor” or “Alice Darji.”
Caution
Note that overuse of the @synchronized
directive can slow down your app, as only one thread can execute within the critical section at any time.
For our case, we chose user
as the object to acquire a lock on. Thus, the updateUser:properties:
method can be called from multiple threads for as many users as necessary, and it will execute with high concurrency as long as the user
objects are not the same. The result is code implemented for high-concurrency use with guards against data corruption.
Tip
The object on which the lock is acquired is key to well-defined critical sections. As a rule of thumb, select the object whose state will be accessed or modified as the reference for the mutex.
So far, so good. But what should the strategy be for reading the properties? What if you needed to display the full name of the HPUser
object while it is being modified?
Locks
Locks are the basic building blocks to enter a critical section. atomic
properties and @synchronized
blocks are higher-level abstractions available for easy use.
There are three kinds of locks available:
NSLock
-
This is a low-level lock. Once a lock is acquired, the execution enters the critical section and no more than one thread can execute concurrently. Release the lock to mark the end of the critical section.
Example 4-5 shows an example of using
NSLock
.Example 4-5. Using NSLock
@interface
ThreadSafeClass
(
)
{
NSLock
*
lock
;
}
@end
-
(
instancetype
)
init
{
if
(
self
=
[
super
init
]
)
{
self
-
>
lock
=
[
NSLock
new
]
;
}
return
self
;
}
-
(
void
)
safeMethod
{
[
self
-
>
lock
lock
]
;
//Thread-safe code
[
self
-
>
lock
unlock
]
;
}
The lock is declared as a
private
field. Another option is to make it a property.Initialize the lock.
Acquire the lock to enter the critical section.
In the critical section, a maximum of one thread can execute at any time.
Release the lock to mark the end of the critical section. Another thread can now acquire the lock.
NSLock
must beunlock
ed from the same thread where it waslock
ed. NSRecursiveLock
-
NSLock
does not allowlock
to be called more than once without first callingunlock
.NSRecursiveLock
, as the name indicates, does allowlock
to be called more than once before it isunlock
ed. Eachlock
call must be matched with an equal number ofunlock
calls before the lock can be considered released for another thread to acquire.NSRecursiveLock
is useful when you have a class with multiple methods that use the same lock to synchronize and one method invokes the other. Example 4-6 shows an example of using it.Example 4-6. Using NSRecursiveLock
@interface
ThreadSafeClass
(
)
{
NSRecursiveLock
*
lock
;
}
@end
-
(
instancetype
)
init
{
if
(
self
=
[
super
init
]
)
{
self
-
>
lock
=
[
NSRecursiveLock
new
]
;
}
return
self
;
}
-
(
void
)
safeMethod1
{
[
self
-
>
lock
lock
]
;
[
self
safeMethod2
]
;
[
self
-
>
lock
unlock
]
;
}
-
(
void
)
safeMethod2
{
[
self
-
>
lock
lock
]
;
//Thread-safe code
[
self
-
>
lock
unlock
]
;
}
The
NSRecursiveLock
object.safeMethod1
acquires the lock.It calls method
safeMethod2
.safeMethod2
acquires a lock on the already-acquired lock.safeMethod2
releases the lock.safeMethod1
releases the lock. Because eachlock
call is now matched with a correspondingunlock
, the lock is now released and ready to be acquired by another thread.
NSCondition
-
There are cases when there is a need to coordinate execution across threads. For example, a thread may want to wait until another thread has results ready.
NSCondition
can be used to atomically release a lock and let it be obtained by another waiting thread, while the original thread waits.A thread can
wait
on a condition that releases the lock. Another thread cansignal
the condition by releasing the same lock and awakening the waiting thread.The standard producer–consumer problem can be solved using
NSCondition
. Example 4-7 shows the code to implement the solution to this problem.Example 4-7. Using NSCondition
@implementation
Producer
-
(
instancetype
)
initWithCondition:
(
NSCondition
*
)
condition
collector:
(
NSMutableArray
*
)
collector
{
if
(
self
=
[
super
init
]
)
{
self
.
condition
=
condition
;
self
.
collector
=
collector
;
self
.
shouldProduce
=
NO
;
self
.
item
=
nil
;
}
return
self
;
}
-
(
void
)
produce
{
self
.
shouldProduce
=
YES
;
while
(
self
.
shouldProduce
)
{
[
self
.
condition
lock
]
;
if
(
self
.
collector
.
count
>
0
)
{
[
self
.
condition
wait
]
;
}
[
self
.
collector
addObject
:
[
self
nextItem
]
]
;
[
self
.
condition
signal
]
;
[
self
.
condition
unlock
]
;
}
}
@end
@implementation
Consumer
-
(
instancetype
)
initWithCondition:
(
NSCondition
*
)
condition
collector:
(
NSMutableArray
*
)
collector
{
if
(
self
=
[
super
init
]
)
{
self
.
condition
=
condition
;
self
.
collector
=
collector
;
self
.
shouldConsume
=
NO
;
self
.
item
=
nil
;
}
return
self
;
}
-
(
void
)
consume
{
self
.
shouldConsume
=
YES
;
while
(
self
.
shouldConsume
)
{
[
self
.
condition
lock
]
;
if
(
self
.
collector
.
count
=
=
0
)
{
[
self
.
condition
wait
]
;
}
id
item
=
[
self
.
collector
objectAtIndex
:
0
]
;
//process item
[
self
.
collector
removeItemAtIndex
:
0
]
;
[
self
.
condition
signal
]
;
[
self
.
condition
unlock
]
;
}
}
@end
@implementation
Coordinator
-
(
void
)
start
{
NSMutableArray
*
pipeline
=
[
NSMutableArray
array
]
;
NSCondition
*
condition
=
[
NSCondition
new
]
;
Producer
*
p
=
[
Producer
initWithCondition
:
condition
collector
:
pipeline
]
;
Consumer
*
c
=
[
Consumer
initWithCondition
:
condition
collector
:
pipeline
]
;
[
[
NSThread
initWithTarget
:
self
selector
:
@
SEL
(
startProducer
)
object
:
p
]
start
]
;
[
[
NSThread
initWithTarget
:
self
selector
:
@
SEL
(
startCollector
)
object
:
c
]
start
]
;
//once done
p
.
shouldProduce
=
NO
;
c
.
shouldConsume
=
NO
;
[
condition
broadcst
]
;
}
@end
The initializer for the producer needs the
NSCondition
object to coordinate with and acollector
to push produced items to. It is initially set to not produce (shouldProduce = NO
).The producer will produce while
shouldProduce
isYES
. Another thread should set it toNO
for the producer to stop producing.Obtain the lock on the
condition
to enter the critical section.If the
collector
already has some not-consumed items,wait
, which blocks the current thread until thecondition
issignal
ed.Add the produced
nextItem
to thecollector
for it to be consumed.signal
anotherwait
ing thread, if any. This is an indicator that an item has been produced, and added to thecollector
, and is available to be consumed.Release the lock.
The initializer for the consumer needs the
NSCondition
object to coordinate with and acollector
to push produced items to. It is initially set to not consume (shouldConsume = NO
).The consumer will consume while
shouldConsume
isYES
. Another thread should set it toNO
for the consumer to stop consuming.Obtain the lock on the
condition
to enter the critical section.If the
collector
has no items,wait
.Consume the next item in the
collector
. Ensure that it is removed from thecollector
.signal
anotherwait
ing thread, if any. This is an indicator that an item has been consumed and removed from thecollector
.Release the lock.
The
Coordinator
class readies the input data for the producer and consumer (specifically, thecollector
and thecondition
).Set up the producer and consumer.
Start production and consumption tasks in different threads.
Once completed, set the producer and consumer to stop producing and consuming, respectively.
Because the producer and consumer threads may be
wait
ing,broadcast
, which is essentiallysignal
ing allwait
ing threads, unlikesignal
, which affects only one of thewait
ing threads.
Use Reader–Writer Locks for Concurrent Reads and Writes
We started this section with two choices for achieving thread safety. We discuss best practices to safeguard against concurrent writes in this section and talk about immutable entities in the next section.
We already learned that atomic
properties safeguard against inconsistent updates and are overcautious about it. If multiple threads attempt to read a property, the synthesized code allows access to only one thread at a time. Having an atomic
property will therefore slow down the app.
This can be a big bottleneck, especially if the state is shared across various components and may need to be accessed from multiple threads. An example of this is a cookie or access token after login. It can change periodically but will be required by all network calls made to the server.
Another use case for such a scenario is the cache. A cache entry can be used anywhere in the app and may be updated upon specific user actions or otherwise.
Essentially, we need a mechanism for concurrent reads but exclusive writes. That brings us to the topic of reader–writer locks. They are also known as multiple readers/single-writer or multireader locks.
A reader–writer lock allows concurrent access for read-only operations, while write operations require exclusive access. This means that multiple threads can read the data in parallel but an exclusive lock is needed to modify the data.
GCD barriers allow creating a synchronization point within a concurrent dispatch queue. When GCD encounters a barrier, the corresponding queue delays the execution of the block until all blocks submitted before the barrier are finished executing. And then, the block submitted via a barrier executes exclusively. We shall call this block a barrier block. Subsequently, the queue continues with its normal execution behavior.
Figure 4-2 demonstrates the effect that barriers have on execution in a multithreaded environment. Blocks 1 through 6 can execute concurrently across multiple threads in the app. However, the barrier block executes exclusively. The only constraint that must be satisfied is that all executions must happen on the same concurrent queue.
To implement this behavior, we need to follow these steps:
-
Create a concurrent queue.
-
Execute all reads using
dispatch_sync
on this queue. -
Execute all writes using
dispatch_barrier_sync
on the same queue.
You can use the code in Example 4-8 to implement a high-throughput thread-safe model.
Example 4-8. Thread-safe, high-throughput model
//HPCache.h
@interface
HPCache
+
(
HPCache
*
)
sharedInstance
;
-
(
id
)
objectForKey:
(
id
)
key
;
-
(
void
)
setObject:
(
id
)
object
forKey:
(
id
)
key
;
@end
//HPCache.m
@interface
HPCache
(
)
@property
(
nonatomic
,
readonly
)
NSMutableDictionary
*
cacheObjects
;
@property
(
nonatomic
,
readonly
)
dispatch_queue_t
queue
;
@end
@implementation
HPCache
-
(
instancetype
)
init
{
if
(
self
=
[
super
init
]
)
{
_cacheObjects
=
[
NSMutableDictionary
dictionary
]
;
_queue
=
dispatch_queue_create
(
kCacheQueueName
,
DISPATCH_QUEUE_CONCURRENT
)
;
}
return
self
;
}
+
(
HPCache
*
)
sharedInstance
{
static
HPCache
*
instance
=
nil
;
static
dispatch_once_t
onceToken
;
dispatch_once
(
&
onceToken
,
^
{
instance
=
[
[
HPCache
alloc
]
init
]
;
}
)
;
return
instance
;
}
-
(
id
)
objectForKey:
(
id
<
NSCopying
>
)
key
{
__block
id
rv
=
nil
;
dispatch_sync
(
self
.
queue
,
^
{
rv
=
[
self
.
cacheObjects
objectForKey
:
key
]
;
}
)
;
return
rv
;
}
-
(
void
)
setObject:
(
id
)
object
forKey:
(
id
<
NSCopying
>
)
key
{
dispatch_barrier_async
(
self
.
queue
,
^
{
[
self
.
cacheObjects
setObject
:
object
forKey
:
key
]
;
}
)
;
}
@end
Notice that the properties have been marked nonatomic
because there is custom code to manage thread safety using a custom queue and barrier.
Use Immutable Entities
This all looks great. But what if there is a need to access state while it is being modified?
For example, what if the cache is being purged but part of the state needs to be used immediately because the user performed an interaction? What if there were a more effective mechanism for state management than multiple components trying to update it simultaneously?
Your team should follow these best practices:
-
Use immutable entities.
This creates a decoupled, scalable system to manage application state. Let’s go through one of the several possible ways to implement this.
The first step is to clearly define the models. For our case study, we define the following three entities:
HPUser
-
Represents a user in the system. A user has a unique
id
, name broken down intofirstName
andlastName
,gender
, anddateOfBirth
. HPAlbum
-
Represents a photo album. A user may have zero or more albums. An album has a unique
id
,owner
,name
,creationTime
,description
, link tocoverPhoto
(the cover photo of the album), andlikes
(users that liked the album). HPPhoto
-
Represents a photo in an album. An album may have zero or more photos. A photo has a unique
id
,album
to which it belongs,user
(the person who uploaded the photo),caption
,url
, andsize
(width and height).
Example 4-9 shows the code for the entity definitions.
Example 4-9. Entities for the case study, representing a user, an album, and a photo
@interface
HPUser
@property
(
nonatomic
,
copy
)
NSString
*
userId
;
@property
(
nonatomic
,
copy
)
NSString
*
firstName
;
@property
(
nonatomic
,
copy
)
NSString
*
lastName
;
@property
(
nonatomic
,
copy
)
NSString
*
gender
;
@property
(
nonatomic
,
copy
)
NSDate
*
dateOfBirth
;
@property
(
nonatomic
,
strong
)
NSArray
*
albums
;
@end
@class
HPPhoto
;@interface
HPAlbum
@property
(
nonatomic
,
copy
)
NSString
*
albumId
;
@property
(
nonatomic
,
strong
)
HPUser
*
owner
;
@property
(
nonatomic
,
copy
)
NSString
*
name
;
@property
(
nonatomic
,
copy
)
NSString
*
description
;
@property
(
nonatomic
,
copy
)
NSDate
*
creationTime
;
@property
(
nonatomic
,
copy
)
HPPhoto
*
coverPhoto
;
@end
@interface
HPPhoto
@property
(
nonatomic
,
copy
)
NSString
*
photoId
;
@property
(
nonatomic
,
strong
)
HPAlbum
*
album
@property
(
nonatomic
,
strong
)
HPUser
*
user
;
@property
(
nonatomic
,
copy
)
NSString
*
caption
;
@property
(
nonatomic
,
strong
)
NSURL
*
url
;
@property
(
nonatomic
,
copy
)
CGSize
size
;
@end
There are multiple ways to define the model and mechanisms to populate the data. Two of the more common options are:
-
Using a custom initializer
-
Using a builder pattern
Each option has its advantages.
Using a custom initializer may mean a long method name, which can result in a nasty call. Think about the method initWithId:firstName:lastName:gender:birthday:
. And this is when we have used only a few of the available attributes in our model. The initializer bloats if five more attributes were added.
Custom initializers also pose backward compatibility problems. A newer model with more attributes will never be backward compatible. However, this also ensures that the app using the updated version of the model knows right at compile time that things have changed.
Using a builder means managing an extra class for it. It will only have setter methods. The builder will also need parallel storage (properties or otherwise) to store all the data needed by the model. The builder will, eventually, use an initializer.
Any update to the model will require a corresponding change to the builder and its backing properties.
The builder pattern is preferred, as it enables backward compatibility and does not break the app even if there are more attributes added to the model. The extra attributes in the later versions of the model will continue to have their default values.
Using the second option, the code looks similar to that given in Example 4-10. This is code adapted from Klaas Pieter’s idea of implementing the builder pattern using blocks.
Example 4-10. Immutable entity using builder
//HPUser.h
@interface
HPUserBuilder
@property
(
nonatomic
,
copy
)
NSString
*
userId
;
@property
(
nonatomic
,
copy
)
NSString
*
firstName
;
@property
(
nonatomic
,
copy
)
NSString
*
lastName
;
@property
(
nonatomic
,
copy
)
NSString
*
gender
;
@property
(
nonatomic
,
copy
)
NSDate
*
dateOfBirth
;
@property
(
nonatomic
,
strong
)
NSArray
*
albums
;
-
(
HPUser
*
)
build
;
@end
@interface
HPUser
//properties
+
(
instancetype
)
userWithBlock:
(
void
(
^
)
(
HPUserBuilder
*
)
)
block
;
@end
@interface
HPUser
(
)
-
(
instancetype
)
initWithBuilder
:
(
HPUserBuilder
*
)
builder
;
@end
@implementation
HPUserBuilder
-
(
HPUser
*
)
build
{
return
[
[
HPUser
alloc
]
initWithBuilder
:
self
]
;
}
@end
@implementation
HPUser
-
(
instancetype
)
initWithBuilder:
(
HPUserBuilder
*
)
builder
{
if
(
self
=
[
super
init
]
)
{
self
.
userId
=
builder
.
userId
;
self
.
firstName
=
builder
.
firstName
;
self
.
lastName
=
builder
.
lastName
;
self
.
gender
=
builder
.
gender
;
self
.
dateOfBirth
=
builder
.
dateOfBirth
;
self
.
albums
=
[
NSArray
arrayWithArray
:
albums
]
;
}
return
self
;
}
+
(
instancetype
)
userWithBlock:
(
void
(
^
)
(
HPUserBuilder
*
)
)
block
{
HPUserBuilder
*
builder
=
[
[
HPUserBuilder
alloc
]
init
]
;
block
(
builder
)
;
return
[
builder
build
]
;
}
@end
//Building the object, an example
-
(
HPUser
*
)
createUser
{
HPUser
*
rv
=
[
HPUser
userWithBlock
:
^
(
HPUserBuilder
*
builder
)
{
builder
.
userId
=
@"
id001
"
;
builder
.
firstName
=
@"
Alice
"
;
builder
.
lastName
=
@"
Darji
"
;
builder
.
gender
=
@"
F
"
;
NSCalendar
*
cal
=
[
NSCalendar
currentCalendar
]
;
NSDateComponents
*
components
=
[
[
NSDateComponents
alloc
]
init
]
;
[
components
setYear
:
1980
]
;
[
components
setMonth
:
1
]
;
[
components
setDay
:
1
]
;
builder
.
dateOfBirth
=
[
cal
dateFromComponents
:
components
]
;
builder
.
albums
=
[
NSArray
array
]
;
}
]
;
return
rv
;
}
The builder.
The model with the class method
userWithBlock:
. Example 4-9 has all the properties declared.Private extension to the model—the custom initializer.
Implementation of the
build
method.Implementation of the custom initializer of the model.
Implementation of the
userWithBlock:
method.A sample use of the builder to create the object.
Note that the preceding code has a few advantages:
-
The model is always backward compatible. A new version of the model-builder with extra attributes will not break the
createUser
code. -
The builder can be created directly. The consumer of the model can instantiate the builder and call the
build
method to create the model object. -
The builder creation and handling can be left to the core. The consumer of the model can use the class method
userWithBlock
: and does not need to either instantiate or call thebuild
method by itself.
Have a Central State Updater Service
The next thing that we need an updater service to update is the client state. The updater service may require connecting to the server, validating the update before performing a local update—for example, adding or updating a record, confirming a friend request, or uploading a photo. From the UI perspective, in the interim, you may show a progress bar or some other indicator to keep the user informed about the status of the change of the state.
For our case, let’s have HPUserService
, HPAlbumService
, and HPPhotoService
classes for servicing HPUser
, HPAlbum
, and HPPhoto
objects, respectively.
Updating state is tricky because it is immutable. Paradoxical, isn’t it? One option is to let the state builder take an input state that can be subsequently modified.
To do that for HPUser
, we can create a helper initializer on HPUserBuilder
that takes an input object.
The code in Example 4-11 shows an updated HPUserBuilder
class to support modifications to an earlier created HPUser
object, and an HPUserService
class to retrieve and update the objects. Similar infrastructure will exist for HPAlbum
and HPPhoto
entities. This code demonstrates the services for user and album entities for the following two scenarios:
-
Retrieving data from the server resulting in an update to local state
-
Updating local and remote states, for example, upon a user interaction
Example 4-11. Services for user and album objects
//HPUserBuilder.h
@interface
HPUserBuilder
-
(
instancetype
)
initWithUser:
(
HPUser
*
)
user
;
@end
@interface
HPUserBuilder
-
(
instancetype
)
initWithUser:
(
HPUser
*
)
user
{
if
(
self
=
[
super
init
]
)
{
self
.
userId
=
builder
.
userId
;
self
.
firstName
=
user
.
firstName
;
self
.
lastName
=
user
.
lastName
;
self
.
gender
=
user
.
gender
;
self
.
dateOfBirth
=
user
.
dateOfBirth
;
self
.
albums
=
user
.
albums
;
}
return
self
;
}
@end
//HPUserService.h
@interface
HPUserService
+
(
instancetype
)
sharedInstance
;
-
(
void
)
userWithId:
(
NSString
*
)
id
completion:
(
void
(
^
)
(
HPUser
*
)
)
completion
;
-
(
void
)
updateUser:
(
HPUser
*
)
user
completion:
(
void
(
^
)
(
HPUser
*
)
)
completion
;
@end
//HPUserService.m
@interface
HPUserService
@property
(
nonatomic
,
strong
)
NSMutableDictionary
*
userCache
;
@end
@implementation
HPUserService
-
(
instancetype
)
init
{
if
(
self
=
[
super
init
]
)
{
self
.
userCache
=
[
NSMutableDictionary
dictionary
]
;
}
return
self
;
}
-
(
void
)
userWithId:
(
NSString
*
)
id
completion:
(
void
(
^
)
(
HPUser
*
)
)
completion
{
//Check in local cache or fetch from server
HPUser
*
user
=
(
HPUser
*
)
[
self
.
userCache
objectForKey
:
id
]
;
if
(
user
)
{
completion
(
user
)
;
}
[
[
HPSyncService
sharedInstance
]
fetchType
:
@"
user
"
withId
:
id
completion
:
^
(
NSDictionary
*
data
)
{
//Use HPUserBuilder, parse data and build
HPUser
*
userFromServer
=
[
builder
build
]
;
[
self
.
userCache
setObject
:
userFromServer
forKey
:
userFromServer
.
userId
]
;
callback
(
userFromServer
)
;
}
]
;
}
-
(
void
)
updateUser:
(
HPUser
*
)
user
completion:
(
void
(
^
)
(
HPUser
*
)
)
completion
{
//May require update to server
[
[
HPSyncService
sharedInstance
]
updateType
:
@"
user
"
//Use HPUserBuilder, parse data and build
HPUser
*
updatedUser
=
[
builder
build
]
;
[
self
.
userCache
setObject
:
updatedUser
forKey
:
updatedUser
.
userId
]
;
[
HPAlbumService
updateAlbums
:
updatedUser
.
albums
]
;
completion
(
updatedUser
)
;
}
]
;
}
@end
HPUserBuilder
now has another custom initializer. It takes anHPUser
object as a parameter and initializes itself with the values from the user object. The state can be modified using property setters and a new object can finally be built using thebuild
method. Note that although the state has been modified, the old object has not been modified. This also means that if the old object is being used in another entity (e.g., a view controller), it has to be replaced. We will explore state change notifications in the next section.HPUserService
follows a singleton pattern here and is available usingsharedInstance
. The code has been omitted for brevity, but we know how to implement good and safe singletons. It is not advisable to use a singleton entity or service levels, as it results in tight coupling and also interferes with mocking frameworks. A configurable factory is preferred over using singletons. The factory may create a disposable singleton. We will revisit this topic in Chapter 10.As a quick prototype, the service also holds on to the cache of user objects created. However, it is definitely not a good idea to mix state with the cache logic. Always keep the state separate from any other intelligent code. You want to keep the models as dumb as possible.
The
HPUserService
initializer has been overridden to initialize the cache. This is a stopgap solution, as the focus of our discussion is about how immutable objects can serve better than mutable objects whose state can be changed from different parts of the app. In a real-world app, the service object will have access to the state, which can be used as input for any processing or be updated, and to underlying network operations to keep the server in sync.A user with a given
id
can be retrieved usinguserWithId:completion:
. If the object exists in the local state, it is returned. Otherwise, it may contact the server and retrieve the details. Once ready, thecompletion
callback is used to notify the caller that the object is available.Availability of a sync service,
HPSyncService
, is assumed here. The service retrieves data from the server. It is also assumed that the server sends a JSON object6 that is deserialized into anNSDictionary
. The code for extracting properties and populating the builder has been omitted. Once the data is available, we also update the local cache so that further server trips can be avoided.User state can be updated using the
updateUser:completion:
method.Updating local state may require syncing changes to the server.
Once the server has been notified, the local cache is updated. Because the user object holds albums, the album service is used to update the related albums as well. Specifically, the associated
owner
object must now point to the updated user object. The old user object must be up fordealloc
. Note that the solution presented here is not scalable: what if other entities also need to update themselves? We will fix this problem momentarily.
One of the points to note in the entities is their cross-references. The user has a list of albums, and each album has an owner. Similarly, an album has a list of photos, and each photo has its container album. And we have not even modeled the comments on a photo, which may comprise when the comment was made, the content, and the user who wrote it.
Regardless of whether they are strong or weak, creating immutable objects with such cross-references has been purposefully omitted here. We need the user object to be ready before the album can be created, and vice versa. It is a catch-22 situation.
The way out is to keep the objects mutable unless specifically marked immutable. This is known as popsicle immutability.7 For this, you may have a special method, say, freeze
or markImmutable
. To be able to use this structure, you will need custom setters that will first check if the object is immutable before allowing any changes.
We can now solve the deadlock. We allow HPAlbum
to be modifiable until we set its owner. We create the HPUser
object and set the owner of the HPAlbum
object. Subsequently, we call the method freeze
on the HPAlbum
object. After all albums are created, we assign them to albums
property of the HPUser
object. Finally, we call the method freeze
on the HPUser
object.
Code to this effect is shown in Example 4-12. HPUser
has been updated to have read/write properties and be mutable until it is marked immutable. And guess what—for most common use cases, you will probably never need a builder because the properties are read/write.
Example 4-12. Popsicle-immutable entities
//HPUser.h
@interface
HPUser
@property
(
nonatomic
,
copy
)
NSString
*
userId
;
@property
(
nonatomic
,
copy
)
NSString
*
firstName
;
-
(
void
)
freeze
;
@end
//HPUser.m
@interface
HPUser
(
)
@property
(
nonatomic
,
copy
)
BOOL
frozen
;
@end
@implementation
HPUser
@synthesize
userId
=
_userId
;
@synthesize
firstName
=
_firstName
;
-
(
void
)
freeze
{
self
.
frozen
=
YES
;
}
-
(
void
)
setUserId:
(
NSString
*
)
userId
{
if
(
!
self
.
frozen
)
{
self
-
>
_userId
=
userId
;
}
}
-
(
void
)
setFirstName:
(
NSString
*
)
firstName
{
if
(
!
self
.
frozen
)
{
self
-
>
_firstName
=
firstName
;
}
}
//... Other setters omitted
@end
//Creating objects
-
(
HPUser
*
)
sampleUser
{
HPUser
*
user
=
[
[
HPUser
alloc
]
init
]
;
user
.
userId
=
@"
user-1
"
;
user
.
firstName
=
@"
Bob
"
;
user
.
lastName
=
@"
Taylor
"
;
user
.
gender
=
@"
M
"
;
HPAlbum
*
album1
=
[
[
HPAlbum
alloc
]
init
]
;
album1
.
owner
=
user
;
album1
.
name
=
@"
Album 1
"
;
//... other properties
[
album1
freeze
]
;
HPAlbum
*
album2
=
[
[
HPAlbum
alloc
]
init
]
;
album2
.
owner
=
user
;
album2
.
name
=
@"
Album 2
"
;
//... other properties
[
album2
freeze
]
;
user
.
albums
=
[
NSArray
arrayWithObjects
:
album1
,
album2
,
nil
]
;
[
user
freeze
]
;
return
user
;
}
The properties are no longer
readonly
. They arereadwrite
(implicit).We add the method
freeze
, which marks an object immutable. Objects are mutable by default.A flag to track the immutability state of the object.
Because we are going write custom setters, we need to
@synthesize
and tell the compiler about the backing iVar to use.Implementation of the method
freeze
marks the object immutable.Custom setters. First, check if the object is mutable. If yes, update. If not, do not update. You may want to throw an exception during development time to ensure legitimate invocations and identify any bad code.
Sample code to demonstrate the use of the new API.
A user is assigned as the album’s owner. At this point, both objects are mutable.
HPAlbum
object marked immutable.HPUser
object marked immutable. Notice how the line just before this can make use of the immutable album objects.
Although the objects may be mutable for a while, we ensure that the mutability is short-lived and restricted only to the thread that created an object. Before the objects are pushed from the creation method to the shared app state, you must ensure that they are marked immutable.
State Observers and Notifications
The previous section left us with an unanswered question: how do we update dependents if an object is updated? Or, put differently, what are the best options to track state changes?
To track changes, you have the following options:
-
KVO
-
The notification center
-
A custom solution
We looked at the first two options briefly in Chapter 2. KVO is great for tracking changes in object properties. But using our approach, this does not work because the objects are immutable and we replace the entire object. As such, the observer will never receive any callbacks.
The notification center is a great option. It serves a useful purpose and would suffice for most of the parts. But the challenge is scaling for the complex scenarios that an app will eventually have—for example, filtering update notifications by album ID or bubbling up the changes to the UI directly if possible.
That’s where a custom solution is needed. And to make that happen, we will switch our style to reactive programming.
Reactive Programming is programming with asynchronous data streams.8 Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc.
The ReactiveCocoa library enables reactive programming in Objective-C. It not only allows observers on arbitrary state but also has advanced category extensions for bubbling them all the way up to UI elements (UILabel
, for example) or responding to interactive views (UIButton
, for example).
We will use ReactiveCocoa to notify observers about any model changes. The observers can be created anywhere.
For illustrative purposes, we will add a notification during each user creation and update operation. We will also add an observer in the album service to monitor any changes to the owner (user) and, for completeness, in the UI to monitor changes to the albums list for a user. Example 4-13 shows the relevant parts of the code.
Example 4-13. Observers and notifications
//HPUserService.m
-
(
RACSignal
*
)
signalForUserWithId:
(
NSString
*
)
id
{
@
weakify
(
self
)
;
return
[
RACSignal
createSignal
:
^
RACDisposable
*
(
id
<
RACSubscriber
>
subscriber
)
{
@
strongify
(
self
)
;
HPUser
*
userFromCache
=
[
self
.
userCache
objectForKey
:
id
]
;
if
(
userFromCache
)
{
[
subscriber
sendNext
:
userFromCache
]
;
[
subscriber
sendCompleted
]
;
}
else
{
//Assuming HPSyncService also follows FRP style
[
[
[
HPSyncService
sharedInstance
]
loadType
:
@"
user
"
withId
:
id
]
subscribeNext
:
^
(
HPUser
*
userFromServer
)
{
//Also update local cache and notify
[
subscriber
sendNext
:
userFromServer
]
;
[
subscriber
sendCompleted
]
;
}
error
:
^
(
NSError
*
error
)
{
[
subscriber
sendError
:
error
]
;
}
]
;
}
return
nil
;
}
]
}
-
(
RACSignal
*
)
signalForUpdateUser:
(
HPUser
*
)
user
{
@
weakify
(
self
)
;
return
[
RACSignal
createSignal
:
^
RACDisposable
*
(
id
<
RACSubscriber
>
subscriber
)
{
//Update the server
[
[
[
HPSyncService
sharedInstance
]
updateType
:
@"
user
"
withId
:
user
.
userId
value
:
user
]
subscribeNext
:
^
(
NSDictionary
*
data
)
{
//Use HPUserBuilder, parse data and build
HPUser
*
updatedUser
=
[
builder
build
]
;
@
strongify
(
self
)
;
var
oldUser
=
[
self
.
userCache
objectForKey
:
updatedUser
.
userId
]
;
[
self
.
userCache
setObject
:
updatedUser
forKey
:
updatedUser
.
userId
]
;
[
subscriber
sendNext
:
updatedUser
]
;
[
subscriber
sendCompleted
]
;
[
self
notifyCacheUpdatedWithUser
:
updatedUser
old
:
oldUser
]
;
}
error
:
^
(
NSError
*
error
)
{
[
subscriber
sendError
:
error
]
;
}
]
;
}
]
;
}
-
(
void
)
notifyCacheUpdatedWithUser:
(
HPUser
*
)
user
old:
(
HPUser
*
)
oldUser
{
NSDictionary
*
tuple
=
{
@"
old
"
:
oldUser
,
@"
new
"
:
user
}
;
[
NSNotificationCenter
.
defaultCenter
postNotificationName
:
@"
userUpdated
"
object
:
tuple
]
;
}
-
(
RACSignal
*
)
signalForUserUpdates:
(
id
)
object
{
return
[
[
NSNotificationCenter
.
defaultCenter
rac_addObserverForName
:
@"
userUpdated
"
object
:
object
]
flattenMap
:
^
(
NSNotification
*
note
)
{
return
note
.
object
;
}
]
;
}
//At some other place in app
-
(
void
)
retrieveAUser:
(
NSString
*
)
userId
{
[
[
[
HPUserService
sharedInstance
]
signalForUserWithId
:
userId
]
subscribeNext
:
^
(
HPUser
*
user
)
{
//process user, maybe update UI
}
error
:
^
(
NSError
*
)
{
//show error to user
}
]
;
}
-
(
void
)
updateAUser:
(
HPUser
*
)
user
{
[
[
[
HPUserService
sharedInstance
]
signalForUpdateUser
:
user
]
subscribeNext
:
^
(
HPUser
*
user
)
{
//process user, maybe update UI
}
error
:
^
(
NSError
*
)
{
//show error to user
}
]
;
}
//Listening for user updates
-
watchForUserUpdates
{
[
[
[
HPUserService
sharedInstance
]
signalForUserUpdates
:
self
]
subcribeNext
:
^
(
NSDictionary
*
tuple
)
{
//Do something with the values
HPUser
*
oldUser
objectForKey
:
@"
old
"
;
HPUser
*
newUser
objectForKey
:
@"
new
"
;
}
]
;
}
The method
signalForUserWithId
does not take in a block as a parameter but returns a promise that can be chained. The@weakify
and@strongify
macros that were first introduced in “Best Practices” have been used here.The code for the signal is pretty much the same as the original code in
userWithId:
but this time usingRACSubscriber
and a promise.It is assumed that the method
loadType:withId
in the classHPSyncService
also returns a promise, anRACSignal
.The method
signalForUpdateUser:
updates anHPUser
object.This creates the
RACSignal
.When the user is updated, you need to not only inform the immediate subscriber, but also notify observers about updates to the cache.
notifyCacheUpdatedWithUser:old:
broadcasts about user object changes.NSNotificationCenter
has been used here for simplicity. This method may not be exposed to theHPUserService
users. It is an extension method.The method published (in the HPUserService.h file) is
signalForUserUpdates:
.It uses the
rac_addObserverForName
category extension provided by the ReactiveCocoa framework to subscribe touserUpdated
notifications. It also extracts the actualNSDictionary
, comprised of theold
andnew
user objects from the underlyingNSNotification
object.The
retrieveAUser:
method demonstrates sample code to retrieve a user.The
subscribeNext:
block is where theuser
object is received.The
updateAUser:
method demonstrates sample code to update a user.The
subscribeNext:
block is where theuser
object is received.The
watchForUserUpdates:
method shows sample code to watch for changes in the user cache.It uses the method
signalForUserUpdates:
to listen to notifications about changes to the user cache.The
subscribeNext:
block is given theNSDictionary
ofold
andnew
objects.The advantage is that if in the future the implementation of
signalForUserUpdates:
changes to not useNSNotificationCenter
, it will not result in changes all the way up towatchForUserUpdates:
.
The primary motive for using this library is that it already has what we need to implement a decoupled, scalable, self-contained, general-purpose system for observing for changes. More importantly, it provides promises for chaining (using RACSignal
) that allow us to write code in a style that is more understandable and maintainable. It also provides simpler solutions for interacting with the UI elements—something that we will use as we continue to build on these concepts in upcoming chapters. In a nutshell, it provides a lot of boilerplate code that we would otherwise have had to write ourselves, and a lot more.
Prefer Async over Sync
In the previous section, we learned that we should prefer promises. This section provides some deeper discussion of asynchronous code.
There is a big and more impactful reason to always prefer async over sync. And it has to do with synchronization. In “Use Reader–Writer Locks for Concurrent Reads and Writes”, we discussed using dispatch barriers and learned about how dispatch_sync
can be used for concurrent reads.
Let’s briefly analyze the code in Example 4-14.
Example 4-14. Using dispatch-sync in the real world
//Case A
dispatch_sync
(
queue
,
^
()
{
dispatch_sync
(
queue
,
^
()
{
NSLog
(
@"nested sync call"
);
});
});
//Case B
-(
void
)
methodA1
{
dispatch_sync
(
queue1
,
^
()
{
[
objB
methodB
];
});
}
-(
void
)
methodA2
{
dispatch_sync
(
queue1
,
^
()
{
NSLog
(
@"indirect nested dispatch_sync"
);
});
}
-(
void
)
methodB
{
[
objA
methodA2
];
}
In Example 4-14, Case A demonstrates a hypothetical scenario in which a nested dispatch_sync
is invoked using the same dispatch queue. This results in a deadlock. The nested dispatch_sync
cannot dispatch into the queue because the current thread is already on the queue and will not release the lock.
Case B demonstrates a more likely scenario. A class has two methods (methodA1
and methodA2
) that use the same queue. The former method calls a methodB
on some object, which in turns calls the latter. The end result is a deadlock. The otherwise useful method dispatch_get_current_queue
has long since been deprecated.11
One option is to use the dispatch_queue_set_specific
and dispatch_get_specific
methods, but you will realize that the code gets murky pretty soon.
For thread-safe, deadlock-free, and maintainable code, using an async style is highly recommended. And there is nothing better than using promises. ReactiveCocoa (see “Functional Reactive Programming and ReactiveCocoa”) introduces the FRP style in Objective-C. dispatch_async
does not suffer from this behavior.
Summary
It is impossible to envision any app without concurrent programming. Operations as simple as animation require multitasking. All long-running tasks (such as networking and I/O) must always be done in a background thread.
With an in-depth analysis on various available options (namely threads, GCD, and operations and queues) in hand, you should now be able to select the one that works best in your specific scenario.
Choosing the right option to make your code thread-safe is key to the correctness of the app’s state. Using mutexes to synchronize access to code blocks is as important as creating high-throughput reads with protected writes using reader–writer locks.
Now that you are familiar with the core optimization techniques for memory management, energy use, and concurrent programming discussed in this part of the book, you should be able to optimize the model and business logic layers of your app.
1 iOS Developer Library, “Thread Costs”.
2 Stack Overflow, “Number of Threads Created by GCD?”.
3 Stack Overflow, “Workaround on the Threads Limit in Grand Central Dispatch?”.
4 Stack Overflow, “GCD Dispatch Concurrent Queue Freeze with ‘Dispatch Thread Soft Limit Reached: 64’ in Crash Log”.
5 Stack Overflow, “What Is an Invariant?”.
6 You may want to explore other formats, such as Protobuf, Thrift, or Avro.
7 Stack Overflow, “How to Design an Immutable Object with Complex Initialization”.
8 The introduction to Reactive Programming you’ve been missing.
9 Facebook, “Making News Feed Nearly 50% Faster on iOS”.
10 “Facebook’s iOS Architecture - @Scale 2014 - Mobile”.
11 dispatch_get_current_queue
, Developer Tools Manual Page.
Get High Performance iOS Apps now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.