humancode.us

Keeping Things Straight with GCD

June 14, 2016

GCD Logo

This is the sixth and final post in a series about Grand Central Dispatch.

Long time users of GCD will tell you: it’s easy to forget where you are. Which queue am I on? Should I dispatch_sync to a queue that protects my variables, or has my caller taken care of that?

In this post I will describe a simple naming convention that has kept me sane over the years. Follow it, and you shouldn’t ever deadlock or forget to synchronize access to your member variables again.

Designing thread-safe libraries

When designing thread-safe code, it helps to have a library mindset. You should distinguish between the external or public interface (API), and the internal or private interface. The public API is presented in public headers, and the private interface is presented in private headers, used only by the library’s developers.

The ideal thread-safe public API does not expose threading or queueing at all (unless, of course, thread or queue management is the point of your library’s utility). It should simply not be possible to induce a race condition or deadlock when using your library. Let’s take a look at this classic example:

// Public header

#import <Foundation/Foundation.h>
// Thread-safe

@interface Account: NSObject
@property (nonatomic, readonly, getter=isClosed) BOOL closed;
@property (nonatomic, readonly) double balance;
- (void)addMoney:(double)amount;
- (void)subtractMoney:(double)amount;
- (double)closeAccount; // Returns balance
@end

@interface Bank: NSObject
@property (nonatomic, copy) NSArray<Account *> *accounts;
@property (nonatomic, readonly) double totalBalanceInAllAccounts;
- (void)transferMoneyAmount:(double)amount
                fromAccount:(Account *)fromAccount
                  toAccount:(Account *)toAccount;
@end

If it weren’t for the helpful comment, you wouldn’t be able to tell from this header that the library is thread-safe. In other words, you’ve defined thread-safety as an implementation detail.

Three Simple Rules

In your implementation, define a serial queue on which all member access can be serialized. In my experience, it’s usually enough to define a simple serial queue for an entire area of functionality, which may later be replaced by a concurrent queue if needed for performance.

// Bank_Private.h
dispatch_queue_t memberQueue();
// Bank.m
#import "Bank_Private.h"
dispatch_queue_t memberQueue() {
    static dispatch_queue_t queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("member queue", DISPATCH_QUEUE_SERIAL);
    });
    return queue;
}

And now, we introduce a naming convention as Rule Number One: Every function and variable that must be serialized on a queue must be prefixed by the queue’s name.

All of the properties of our Account class, for example, must be serialized, so their ivars need to be prefixed. One convenient way to do this is to introduce a private class extension:

// Bank_Private.h
@interface Account()
@property (nonatomic, getter=memberQueue_isClosed) BOOL memberQueue_closed;
@property (nonatomic) double memberQueue_balance;
@end

This class extension should appear in the class’s private header.

We’ve changed balance to a readwrite property in the private header, so we can easily change its value from within our library.

Because Objective-C automatically synthesizes ivars and accessors for all properties, we now end up with duplicate ivars: one for the public properties, and one for the private, member-queue-protected ones. One way to prevent autosynthesis for the public properties is to declare them @dynamic in the class’s implementation.

// Bank.m

@implementation Account
@dynamic closed, balance;
@end

We manually provide accessors for the public properties:

// Bank.m
@implementation Account
// ...
- (BOOL)isClosed {
    __block BOOL retval;
    dispatch_sync(memberQueue(), ^{
        retval = self.memberQueue_isClosed;
    });
    return retval;
}

- (double)balance {
    __block double retval;
    dispatch_sync(memberQueue(), ^{
        retval = self.memberQueue_balance;
    });
    return retval;
}
@end

Yes, you can defeat authosynthesis by providing a getter and (for readwrite properties) a setter. But I prefer to state explicitly that I don’t want an ivar, getter, or setter created for me, ever, by using @dynamic. Better to crash during testing due to an unimplemented selector than to have a latent error in released code.

See the pattern? That brings us to Rule Number Two: Only access queue-prefixed variables and functions from blocks enqueued on that queue.

Now let’s implement addMoney:, subtractMoney: and closeAccount to round out the Account class. We’re actually going to write two versions of each function: one that assumes we are not on the member queue, and one that does. Here we go:

// Bank.m
@implementation Account
//...
- (void)addMoney:(double)amount {
    dispatch_sync(memberQueue(), ^{
        [self memberQueue_addMoney:amount];
    });
}
- (void)memberQueue_addMoney:(double)amount {
    self.memberQueue_balance += amount;
}

- (void)subtractMoney:(double)amount {
    dispatch_sync(memberQueue(), ^{
        [self memberQueue_subtractMoney:amount];
    });
}
- (void)memberQueue_subtractMoney:(double)amount {
    self.memberQueue_balance -= amount;
}

- (double)closeAccount {
    __block double retval;
    dispatch_sync(memberQueue(), ^{
        retval = [self memberQueue_closeAccount];
    });
    return retval;
}
- (double)memberQueue_closeAccount {
    self.memberQueue_closed = YES;
    double balance = self.memberQueue_balance;
    self.memberQueue_balance = 0.0;
    return balance;
}

@end

We also publish the prefixed versions of these functions in our private header:

// Bank_Private.h
@interface Account()
//...
- (void)memberQueue_addMoney:(double)amount;
- (void)memberQueue_subtractMoney:(double)amount;
- (double)memberQueue_closeAccount;

This brings us to Rule Number Three: Code inside a prefixed function must only touch other functions and variables that are prefixed by the same queue’s name.

These three rules keep us sane: You know exactly what known queue (if any) you are on, and once you are on that queue, you only access functions and member variables that already assume you are on that queue.

Note how we can modify both memberQueue_closed and memberQueue_balance atomically inside memberQueue_closeAccount, knowing that the function is only called when we are serialized on memberQueue. The increment and decrement operators inside memberQueue_addMoney: and memberQueue_subtractMoney:, too, can safely perform read-modify-write operations without fear of race conditions.

One more time

We can now use objects of the Account class from any thread we want. Now let’s make Bank objects thread-safe as well. Because we are using a single memberQueue for both Bank and Account objects, our job is relatively easy.

Let’s review the Three Rules:

  1. The name of every function and variable that must be accessed only on a queue must be prefixed by the queue name.
  2. Only access queue-prefixed variables and functions from blocks enqueued on that queue.
  3. Code inside a prefixed function must only touch other functions and variables that are prefixed by the same queue’s name.

First, we declare queue-prefixed properties and methods in our private header:

// Bank_Private.h
@interface Bank()
@property (nonatomic, copy) NSArray<Account *> *memberQueue_accounts;
@property (nonatomic, readonly) double memberQueue_totalBalanceInAllAccounts;
- (void)memberQueue_transferMoneyAmount:(double)amount
                            fromAccount:(Account *)fromAccount
                              toAccount:(Account *)toAccount;
@end

We suppress autosynthesis by declaring public properties @dynamic:

// Bank.m
@implementation Bank
@dynamic accounts, totalBalanceInAllAccounts;
@end

We define our member functions:

// Bank.m
@implementation Bank
//...
- (NSArray<Account *> *)accounts {
    __block NSArray<Account *> *retval;
    dispatch_sync(memberQueue(), ^{
        retval = self.memberQueue_accounts;
    });
    return retval;
}
- (void)setAccounts:(NSArray<Account *> *)accounts {
    dispatch_sync(memberQueue(), ^{
        self.memberQueue_accounts = accounts;
    });
}

- (double)totalBalanceInAllAccounts {
    __block double retval;
    dispatch_sync(memberQueue(), ^{
        retval = self.memberQueue_totalBalanceInAllAccounts;
    });
    return retval;
}
- (double)memberQueue_totalBalanceInAllAccounts {
    __block double retval = 0.0;
    for (Account *account in self.memberQueue_accounts) {
        retval += account.memberQueue_balance;
    }
    return retval;
}

- (void)transferMoneyAmount:(double)amount
                fromAccount:(Account *)fromAccount
                  toAccount:(Account *)toAccount {
    dispatch_sync(memberQueue(), ^{
        [self memberQueue_transferMoneyAmount:amount
                                  fromAccount:fromAccount
                                    toAccount:toAccount];
    });
}
- (void)memberQueue_transferMoneyAmount:(double)amount
                            fromAccount:(Account *)fromAccount
                              toAccount:(Account *)toAccount {
    fromAccount.memberQueue_balance -= amount;
    toAccount.memberQueue_balance += amount;
}

And we’re done. The naming convention makes it abundantly clear which operations are performed safely on the serialization queue, and which are not.

Single queue simplicity

This naming pattern has worked very well for me, but it does have limitations. Generally, the system works great if you only have a single serial queue. Luckily, I haven’t found many occasions to use anything else.

Avoid premature optimization. Begin coding with a single serial queue to synchronize access in a cluster of related classes. Change this only when you see performance bottlenecks.

Readers-writer lock

To support concurrent reader-writer queues, you’d need two prefixes for your functions: memberQueue_ and memberQueueMutating_. Non-mutating functions must only read from protected variables, and must only call other non-mutating functions. Mutating functions may read from or write to protected variables, and may call both mutating and non-mutating functions. Use dispatch_sync or dispatch_async to schedule non-mutating function calls, and use dispatch_barrier_sync or dispatch_barrier_async to schedule mutating functions.

Just say no to multiple, nested queues

If you ever find yourself adding awareness of more than one synchronization queue to your class, you’ve probably goofed your architecture.

You normally find yourself needing to deal with two queues when some part of your app accesses a class that uses an “outer” queue (for instance, Bank may have its own queue), but other parts of your app also directly use a class that uses an “inner” queue (calling Account directly). For certain methods like -[Bank transferMoney:...], it may be necessary to serialize on both queues to prevent direct changes to Account objects while Bank is transferring money. This is a sure sign of a design error.

In my experience, it’s never been worth having multiple synchronization queues in a class collection that implement a single function. For performance tuning, turning the serial queue into a concurrent one is usually sufficient.

Exercises for the reader

  • Have -[Bank transferMoney:...] perform checks to prevent withdrawal from a closed account, or overdrafting. How would you change the public and private interfaces to communicate such errors?
  • Implement broadcasting an “account changed” notification using NSNotificationCenter. How would you implement this without risking deadlock?
  • Assume the bank has millions of accounts. Reimplement totalBalanceInAllAccounts to be asynchronous, taking a completion block. What performance challenges might you come across? On what queue should you call the completion block to prevent deadlock?

Prologue

I hope this simple technique will keep your code clean and maintainable, and keep you out of threading trouble. It’s certainly done that for me.

This is the last post in my series on Grand Central Dispatch. I hope you’ve learned something from it! If you like it, please spread the word.