Writing Thread-Safe Classes with GCD
This is the fifth post in a series about Grand Central Dispatch.
So far, we’ve learned that a multi-threaded program means that access to our data structures must be protected by some kind of synchronization mechanism. We used GCD’s serial queues to gate access to our data structures.
We also discussed using concurrent queues to implement a Readers-Writer lock. To keep this post simple, we’ll stick to using serial queues.
One serious issue remains: whoever uses your data structures have to remember to use dispatch_sync()
, otherwise you get errors. This is obviously a problem, especially when your code is used by someone not familiar with your arrangement (for instance, when your code is part of a framework).
Wouldn’t it be great if we could encapsulate the synchronization behavior into our data structure, so that its users don’t have to worry about asynchronous behavior?
Encapsulation, of course, is what classes are really good for. Instead of requiring synchronization code in our user’s codebase, we should make thread-safe classes.
Thread-Safe Classes
What makes a class thread-safe? Simply put, a class is thread-safe if it allows a program to instantiate it, destroy it, access its properties, and send it messages from any thread without fear of multithreading-related errors.
Not every class should be thread-safe! Thread safety incurs performance cost, and is not necessary in many cases. You should decide on the right set of “synchronization points” in your design, where objects “beneath” those points do not need to be thread-safe.
Let’s make the following class thread-safe.
@interface Warrior: NSObject
@property (nonatomic, strong) NSString *leftHandEquippedItem;
@property (nonatomic, strong) NSString *rightHandEquippedItem;
- (void)swapLeftAndRightHandEquippedItems;
- (NSString *)juggleNewItem:(NSString *)item; // return dropped item
@end
We use the nonatomic
attribute in our property specification because we don’t want the compiler to generate automatically-synchronized versions of our properties. We need to specify this attribute because all properties are atomic
by default.
NSString
properties should really be declared with the copy
attribute instead of strong
, but we’re trying to keep things simple for this example.
A non-thread-safe implementation of swapLeftAndRightHandEquippedItems
may look like this:
@implementation Warrior
- (void)swapLeftAndRightHandEquippedItems {
NSString *oldLeftHandEquippedItem = self.leftHandEquippedItem;
self.leftHandEquippedItem = self.rightHandEquippedItem;
self.rightHandEquippedItem = oldLeftHandEquippedItem;
}
@end
It should be immediately obvious that this class isn’t thread-safe. If a second thread were to assign a different rightHandEquippedItem
right as the current thread is swapping items, errors would occur.
Queue to the rescue
We need a queue to help serialize access to our ivars. Because GCD queues are relatively cheap, we are going to create one queue per instance.
@interface Warrior()
@property (nonatomic, strong) dispatch_queue_t memberQueue;
@end
@implementation Warrior
- (id)init {
self = [super init];
if (self) {
_memberQueue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
// ...
@end
The anonymous ()
category is a convenient place to declare private methods and properties. This category should be declared in your implementation (.m
) file.
Now we need to serialize access to the leftHandEquippedItem
and rightHandEquippedItem
properties. We can do it by overriding their getters and setters:
@implementation Warrior
- (NSString *)leftHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = _leftHandEquippedItem;
});
return retval;
}
- (void)setLeftHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
_leftHandEquippedItem = item;
});
}
// Same for right hand...
You can’t assign to variables outside of a block from within the block, unless they are declared with the __block
attribute.
This solves our synchronization problem, but I like to go a step further and declare internal versions of these properties whose names are decorated with queue names.
@interface Warrior()
@property (nonatomic, strong) NSString *memberQueueLeftHandEquippedItem;
@property (nonatomic, strong) NSString *memberQueueRightHandEquippedItem;
// ...
@end
@implementation Warrior
- (NSString *)leftHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueLeftHandEquippedItem;
});
return retval;
}
- (void)setLeftHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
self.memberQueueLeftHandEquippedItem = item;
});
}
// Same for right hand...
@end
Why bother doing this? For one thing, it makes it easy to implement swapLeftAndRightHandEquippedItems
method:
- (void)memberQueueSwapLeftAndRightHandEquippedItems {
NSString *oldLeftHandEquippedItem = self.memberQueueLeftHandEquippedItem;
self.memberQueueLeftHandEquippedItem = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = oldLeftHandEquippedItem;
}
- (void)swapLeftAndRightHandEquippedItems {
dispatch_sync(self.memberQueue, ^{
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
}
See the pattern? This naming scheme helps to keep your head on straight as your class grows to dozens of properties and methods. Without them, you’ll always be left wondering whether you need to dispatch_sync
to a queue before using these properties or methods.
The naming rules are simple: Properties and methods that begin with a queue name can assume that they’re already serialized on that queue.
It’s bad form to refer directly to ivars (such as _myProperty
) from anything other than init
, dealloc
, or the properties’ getters and setters. Besides, using the self.myProperty
form of property access makes your code easier to understand.
This naming scheme allows you to compose atomic actions without confusion.
@implementation Warrior
- (NSString *)juggleNewItem:(NSString *)item {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = item;
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
return retval;
}
// ...
@end
Because you’re already synchronized on memberQueue
inside the dispatch_sync
block, it’s safe to use any properties or methods that begin with memberQueue
. (In fact, you must use properties or methods that begin with memberQueue
, otherwise you will deadlock!)
See how this naming convention keeps things straight?
Let’s say you want to log whenever the equipped items change:
@implementation Warrior
- (void)setMemberQueueLeftHandEquippedItem:(NSString *)item {
NSLog(@"Left hand now holds %@", item);
_memberQueueLeftHandEquippedItem = item;
}
// Same for right hand...
Simple.
The big picture
Here is the completed class:
// Header file
@interface Warrior: NSObject
@property (nonatomic, strong) NSString *leftHandEquippedItem;
@property (nonatomic, strong) NSString *rightHandEquippedItem;
- (void)swapLeftAndRightHandEquippedItems;
- (NSString *)juggleNewItem:(NSString *)item; // return dropped item
@end
// Implementation file
@interface Warrior()
@property (nonatomic, strong) dispatch_queue_t memberQueue;
@property (nonatomic, strong) NSString *memberQueueLeftHandEquippedItem;
@property (nonatomic, strong) NSString *memberQueueRightHandEquippedItem;
@end
@implementation Warrior
- (id)init {
self = [super init];
if (self) {
_memberQueue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)setMemberQueueLeftHandEquippedItem:(NSString *)item {
NSLog(@"Left hand now holds %@", item);
_memberQueueLeftHandEquippedItem = item;
}
- (void)setMemberQueueRightHandEquippedItem:(NSString *)item {
NSLog(@"Right hand now holds %@", item);
_memberQueueRightHandEquippedItem = item;
}
- (NSString *)leftHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueLeftHandEquippedItem;
});
return retval;
}
- (void)setLeftHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
self.memberQueueLeftHandEquippedItem = item;
});
}
- (NSString *)rightHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueRightHandEquippedItem;
});
return retval;
}
- (void)setRightHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
self.memberQueueRightHandEquippedItem = item;
});
}
- (void)memberQueueSwapLeftAndRightHandEquippedItems {
NSString *oldLeftHandEquippedItem = self.memberQueueLeftHandEquippedItem;
self.memberQueueLeftHandEquippedItem = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = oldLeftHandEquippedItem;
}
- (void)swapLeftAndRightHandEquippedItems {
dispatch_sync(self.memberQueue, ^{
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
}
- (NSString *)juggleNewItem:(NSString *)item {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = item;
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
return retval;
}
@end
Note that the public interface of the class hasn’t changed at all from our non-thread-safe version. This is how you know you’ve done things right: All of the thread-safe code is internal to the class, and your users can use your class without any special knowledge of asynchronous behavior.
Exercise for the reader: Define a new readonly
property called bagOfCarrying
that returns an NSArray
. Provide methods to place items into and remove items from this property in a thread-safe way. (Hint: internally, declare this property as memberQueueBagOfCarrying
of type NSMutableArray
.) How should you return the object from the bagOfCarrying
getter to avoid threading issues after the property is retrieved?
Hope this helps! I’ll see you next time when we’ll talk about designing classes that can generate callback events.