Skip to content
Robbie Hanson edited this page Oct 14, 2013 · 26 revisions

YapDatabase was designed with concurrency in mind. But that doesn't mean its impossible to shoot yourself in the foot. Arm yourself with knowledge so you never have any "accidents".


Connections, queues & deadlock

One of the powerful features of the YapDatabase architecture is that connections are thread-safe. That means that you can share a single YapDatabaseConnection amongst multiple threads. For example:

- (void)asyncSolveProblem:(Problem *)p forKey:(NSString *)key
{
    // Solve complex math problem on a background thread (in the thread pool)
    dispatch_queue_t concurrentQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(concurrentQ, ^{
        
        Answer *a = [complexMath solveProblem:p];
        
        // Now save the answer to the database.
        // The databaseConnection is thread-safe so this is safe,
        // even though other threads my be using it simultaneously.
        [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
            
            [transaction setObject:a forKey:key];
        }];
    });
}

All connections have an internal serial dispatch queue. And all operations on a connection (such as executing a transaction) go through this internal serial queue. So its rather easy to conceptualize the nature of the thread-safety within a single connection: All transactions executed on connectionA will execute in a serial fashion.

The main thing to watch out for is executing a transaction within a transaction. This is not allowed, and will result in DEADLOCK :

- (void)deadlock1
{
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        
        id deadlock = [self deadlock2];
    }];
}

- (id)deadlock2
{
    __block id uh_oh = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        
        uh_oh = [transaction objectForKey:@"deadlock"];
    }];

    return uh_oh;
}

This is the most common reason we hear deadlock reports. Now that you understand the problem, I'm sure you can come up with multiple solutions. We'll present one such solution here. Not because its the best solution, but rather because its one that is thought of the least.

- (float)estimatedCostsForAddress:(Address *)addr
{
    __block float taxes = 0.0;

    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
        
        taxes += [self propertyTaxesForAddress:addr withTransaction:transaction];
        taxes += [self hoaFeesForAddress:addr withTransaction:transaction];
    }];

    return taxes;
}

- (float)propertyTaxesForAddress:(Address *)addr withTransaction:(YapDatabaseReadTransaction *)transaction
{
    NSString *key = [NSString stringWithFormat:@"%d", addr.zip];

    return [[transaction objectForKey:key] floatValue];
}

Yup, it's safe to pass a transaction object to helper methods.

Remember:

  • This is just one example of a solution.
  • A transaction object should never be saved as an ivar. That won't work.

Object mutability vs thread-safety

As an Objective-C developer, you're likely familiar with the concept of mutable vs immutable objects. There are multiple classes in Apple's Foundation Framework that distinguish between the two:

  • NSArray vs NSMutableArray
  • NSDictionary vs NSMutableDictionary
  • NSSet vs NSMutableSet

And from Apple's Threading Programming Guide:

Immutable objects are generally thread-safe. Once you create them, you can safely pass these objects to and from threads. On the other hand, mutable objects are generally not thread-safe. To use mutable objects in a threaded application, the application must synchronize appropriately.

It's important to keep this in mind when using YapDatabase. For example, consider the following code:

- (void)someMethodOnMainThread
{
    __block Person *person = nil;
    [connection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        person = [transaction objectForKey:personId];
    }];
    
    // Accessing children array on main thread...
    for (Person *child in person.children)
    {
        [self addViewForChild:child];
    }
}

- (void)someMethodOnBackgroundThread
{
    [connection readWithBlock:^(YapDatabaseReadTransaction *transaction){

        Person *person = [transaction objectForKey:personId];
        
        // Modifying children array on background thread...
        [person.children addObject:newChild];
    }];
}

Recall that every connection has a cache. The cache is important for performance, and drastically reduces both trips to the disk, and the overhead of deserializing an object. Thus its highly likely that both the main thread and background thread are fetching the exact same Person instance. And so the background thread may be modifying the object while the main thread is simultaneously attempting to use it. This is no different than modifying an NSMutableArray from multiple threads without any locks around it.

If the objects you put in the database are mutable you must follow all the same rules & guidelines that you would for any other mutable object.

The recommended practice is to make copies of your objects before you modify them. For example:

- (void)someMethodOnMainThread
{
    __block Person *person = nil;
    [connection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        person = [transaction objectForKey:personId];
    }];
    
    for (Person *child in person.children)
    {
        [self addViewForChild:child];
    }
}

- (void)someMethodOnBackgroundThread
{
    [connection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){

        Person *person = [transaction objectForKey:personId];
        person = [person copy];
        [person.children addObject:newChild];
        
        [transaction setObject:person forKey:personId];
    }];
}

If you follow this simple guideline, you generally won't have to worry about thread-safety. Even if your objects are actually mutable.

In fact YapDatabase has tools to help enforce thread-safety. More on this in moment. But first we need to discuss how objects may get shared between multiple connections.

Sharing objects between connections

Consider the following common scenario:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    __block IMMessage *message = nil;
    
    // Access IMMessage on main thread (via uiConnection)
    [uiConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        onSaleItem = [[transaction ext:@"view"] objectAtIndex:indexPath.row inGroup:conversationId];
    }];
    
    // configure and return cell...
}

- (void)didReceiveDeliveryReceiptForMessageId:(NSString *)messageId
{
    // Update IMMessage on background thread (via bgConnection)
    [bgConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
    
        IMMessage *message = [transaction objectForKey:messageId];
        
        // As per the recommended best practice, we copy the object before modifying it.
        message = [message copy];
        message.delivered = YES;

        [transaction setObject:message forKey:messageId];
    }];
}

In the example code above, an object is being updated on 'bgConnection', but it will immediately be needed on the 'uiConnection' in order to redraw the corresponding cell in the tableView. As you might imagine, this is a very common scenario.

If all connections operate as separate "islands", then this would require the 'uiConnection' to refetch the serialized object from disk, and then deserialize it, in order to fetch the updated version of the object.

YapDatabase can do better. It can be much more efficient than this.

You'll notice in the example above that the code is following the recommended practice of copying objects before modifying them. Thus the updated object, once saved to the database, is thread-safe. That is, other threads/connections have promised to never directly modify that object. Instead they promise to first make copies, and then only modify the copies.

Thus, if this promise is kept (or if YapDatabase has cool tools to enforce this promise), then YapDatabase can drastically give itself a huge performance boost by directly passing the updated object to other connections. And the other connections can then update their cache in-place.

By default, YapDatabase will share objects between connections. There are tools and best practices to ensure this is thread-safe. Following these techniques will result in a nice performance boost. Alternatively, this sharing can be disabled.

What exactly do you mean by "sharing objects between connections" ?

Say there are 2 connections: connectionA and connectionB. Both connectionA and connectionB have their own separate caches. And say both connections have, in their cache, the object for key @"abc123". Then connectionA executes a read-write transaction, makes a copy of said object, modifies it, and then saves it back into the database. What will happen is that, connectionB, when it updates to this latest commit, it will internally process a "change-set" from connectionA. And in doing so, it will update the contents of its own cache. Specifically, it will replace the object for key @"abc123" with the object from the "change-set" that was passed from connectionA. Thus both connectionA and connectionB will have, in their separate caches, the same exact object instance for key @"abc123". (Both connections will have a reference (in their cache) to the same object in memory for key @"abc123".)

Enforcing immutability using the sanitizer

As noted above, the recommended best practice is to makes copies of database objects before modifying them. There are multiple reasons this is recommended:

  • it's straight-forward and easy to do
  • it allows all your database objects to be passed among threads
  • it allows your database objects to be shared between connections
  • it's enforceable using a few tricks

In addition to having a serializer & deserializer, YapDatabase supports a sanitizer:

typedef id (^YapDatabaseSanitizer)(NSString *key, id object);

- (id)initWithPath:(NSString *)path
        serializer:(YapDatabaseSerializer)serializer
      deserializer:(YapDatabaseDeserializer)deserializer
         sanitizer:(YapDatabaseSanitizer)sanitizer;

Whenever you invoke setObject:forKey:, the sanitizer will automatically be run on the given object, before it enters the database system.

For a simple example, assume all objects you put into the database are NSString's. For example:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){

    NSString *fortune = [transaction objectForKey:fortuneCookieId];
    
    NSMutableString *updatedFortune = [fortune mutableCopy];
    [updatedFortune appendString:@" in bed"];

    [transaction setObject:updatedFortune forKey:fortuneCookieId];
}];

You'll notice that we're actually storing an NSMutableString. We could use the sanitizer to ensure that all string objects we store into the database are immutable. This ensures that we won't later forget about thread-safety, and start casting and mutating the string.

YapCollectionsDatabaseSanitizer sanitizer = NULL;
#if DEBUG
sanitizer = ^(NSString *collection, NSString *key, id object){
    
    if ([object isKindOfClass:[NSString class]])
        return [object copy];
    else
        return object;
};
#endif

You'll notice that we only enabled the sanitizer for DEBUG builds. This is because we really only need this for debugging purposes. In production we can turn it off, confident that any mutation attempts would have failed during testing.

Of course, this was a simple example. In real life, you will likely be storing your own custom objects. And it may be overkill to have separate mutable vs immutable versions! Not to mention the extra work involved. All we really want to do is mark an object as immutable. And again, we really only need this for debugging purposes.

Here is a template you can use to mark custom objects as immutable.
Just have your custom objects extend this class instead of NSObject.

MyDatabaseObject.h

#import <Foundation/Foundation.h>


@interface MyDatabaseObject : NSObject <NSCopying>

@property (nonatomic, readonly) BOOL isImmutable;

- (void)makeImmutable;

- (NSException *)immutableExceptionForKey:(NSString *)key;

+ (NSMutableSet *)immutableProperties;

@end

MyDatabaseObject.m

#import "MyDatabaseObject.h"
#import <objc/runtime.h>


@implementation MyDatabaseObject {
@private

    BOOL isImmutable;
}

@synthesize isImmutable = isImmutable;

#pragma mark NSCopying

/**
 * All copies are automatically mutable.
**/
- (id)copyWithZone:(NSZone *)zone
{
    // You can optionally call this method via [super copyWithZone:zone].
    //
    // But since this method only sets isImmutable to NO (the default value),
    // doing so is optional.

    id copy = [[[self class] alloc] init];
    ((MyDatabaseObject *)copy)->isImmutable = NO;

    return copy;
}

#pragma mark Logic

- (void)makeImmutable
{
    if (!isImmutable)
    {
        // Set immutable flag
        isImmutable = YES;

        // Turn on KVO for object.
        // We do this so we can get notified if the user is about to make changes to one of
        // the object's properties.
        //
        // Don't worry, this doesn't create a retain cycle.

        [self addObserver:self forKeyPath:@"isImmutable" options:0 context:NULL];
    }
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"isImmutable"])
        return YES;
    else
        return [super automaticallyNotifiesObserversForKey:key];
}

+ (NSSet *)keyPathsForValuesAffectingIsImmutable
{
    // In order for the KVO magic to work, we specify that the isImmutable property is dependent
    // upon all other properties in the class that should become immutable..
    //
    // The code below ** attempts ** to do this automatically.
    // It does so by creating a list of all the properties in the class.
    //
    // Obviously this will not work for every situation.
    // In particular:
    //
    // - if you have custom setter methods that aren't specified as properties
    // - if you have other custom methods that modify the object
    //
    // To cover these edge cases, simply add code like the following at the beginning of such methods:
    //
    // - (void)recalculateFoo
    // {
    //     if (self.isImmutable) {
    //         @throw [self immutableExceptionForKey:@"foo"];
    //     }
    //
    //     // ... normal code ...
    // }
    
    return [self immutableProperties];
}

+ (NSMutableSet *)immutableProperties
{
    // This method returns a list of all properties that should be considered immutable once
    // the makeImmutable method has been invoked.
    //
    // By default this method returns a list of all properties in each subclass in the
    // hierarchy leading to "[self class]".
    //
    // However, this is not always exactly what you want.
    // For example, if you have any properties which are simply used for caching.
    //
    // @property (nonatomic, strong, readwrite) UIImage *avatarImage;
    // @property (nonatomic, strong, readwrite) UIImage *cachedTransformedAvatarImage;
    //
    // In this example, you store the user's plain avatar image.
    // However, your code transforms the avatar in various ways for display in the UI.
    // So to reduce overhead, you'd like to cache these transformed images in the user object.
    // Thus the 'cachedTransformedAvatarImage' property doesn't actually mutate the user object.
    // It's just temporary.
    //
    // So your subclass would override this method like so:
    //
    // + (NSMutableSet *)immutableProperties
    // {
    //     NSMutableSet *immutableProperties = [super immutableProperties];
    //     [immutableProperties removeObject:@"cachedTransformedAvatarImage"];
    //
    //     return immutableProperties;
    // }
    
    NSMutableSet *dependencies = nil;
    
    Class rootClass = [MyDatabaseObject class];
    Class subClass = [self class];
    
    while (subClass != rootClass)
    {
        unsigned int count = 0;
        objc_property_t *properties = class_copyPropertyList(subClass, &count);
        if (properties)
        {
            if (dependencies == nil)
                dependencies = [NSMutableSet setWithCapacity:count];

            for (unsigned int i = 0; i < count; i++)
            {
                const char *name = property_getName(properties[i]);
                NSString *property = [NSString stringWithUTF8String:name];

                [dependencies addObject:property];
            }

            free(properties);
        }

        subClass = [subClass superclass];
    }

    return dependencies;
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    // Nothing to do
}

- (void)willChangeValueForKey:(NSString *)key
{
    if (isImmutable)
    {
        @throw [self immutableExceptionForKey:key];
    }

    [super willChangeValueForKey:key];
}

- (void)dealloc
{
    if (isImmutable)
    {
        [self removeObserver:self forKeyPath:@"isImmutable" context:NULL];
    }
}

- (NSException *)immutableExceptionForKey:(NSString *)key
{
    NSString *reason;
    if (key)
        reason = [NSString stringWithFormat:@"Attempting to mutate immutable object. Class = %@, property = %@",
                    NSStringFromClass([self class]), key];
    else
        reason = [NSString stringWithFormat:@"Attempting to mutate immutable object. Class = %@",
                    NSStringFromClass([self class])];

    NSDictionary *userInfo = @{ NSLocalizedRecoverySuggestionErrorKey:
                @"To make modifications you should create a copy via [object copy]."
                @" You may then make changes to the copy before saving it back to the database."};

    return [NSException exceptionWithName:@"MyDatabaseObjectException" reason:reason userInfo:userInfo];
}

@end

The template class works rather simply. It observes changes to itself via KVO, and throws an exception if modifications are attempted on an object which has been marked immutable. For example:

#import "MyDatabaseObject.h"

@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@end

@implementation Car
@synthesize make;
@synthesize model;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    Car *car = [[Car alloc] init];
    car.make = @"Tesla";
    car.model = @"Model S";

    [car makeImmutable];

    car.make = @"Ford"; // Throws exception: Attempting to mutate immutable object...
}

So, using this base class, we can easily mark all of our objects as immutable when they go into the database. We can do this via the sanitizer. And we can similarly mark the objects as immutable when they're coming out of the database via the deserializer. And we can do all this conditionally, only for DEBUG builds.

YapDatabaseDeserializer deserializer;
YapCollectionsDatabaseSanitizer sanitizer;

#if DEBUG
deserializer = ^(NSString *key, NSData *data){

    id object = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    if ([object isKindOfClass:[MyDatabaseObject class]])
        [object makeImmutable];

    return object;
};
	
sanitizer = ^(NSString *key, id object){

    if ([object isKindOfClass:[MyDatabaseObject class]])
        [object makeImmutable];

    return object;
};
#elif

deserializer = NULL; // Use default deserializer (NSCoding)
sanitizer = NULL; // Don't use a sanitizer	
#endif

As noted in the comments of the template class, if you have custom setters then you'll need to add checks for immutability. This handles mostly every case except one: mutable ivars.

For example:

#import "MyDatabaseObject.h"

@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@property (nonatomic, strong, readwrite) NSMutableArray *passengers; // danger...
@end

@implementation Car
@synthesize make;
@synthesize model;
@synthesize passengers;
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    Car *car = [[Car alloc] init];
    car.make = @"Tesla";
    car.model = @"Model S";
    car.passengers = [NSMutableArray arrayWithObjects:@"Harry", @"Hermione", nil];

    [car makeImmutable];

    [car.passengers addObject:@"Ron"]; // No exception!
}

The solution is straight-forward: use immutable properties when possible. It's good defensive programming anyway, especially in a concurrent system.

Note: A copy operation on an immutable object just returns 'self'. That is, it doesn't actually copy any bytes.

#import "MyDatabaseObject.h"

@interface Car : MyDatabaseObject
@property (nonatomic, copy, readwrite) NSString *make;
@property (nonatomic, copy, readwrite) NSString *model;
@property (nonatomic, copy, readwrite) NSArray *passengers; // Fixed to be Thread-Safe
@end

Opting out of object sharing

You can optionally opt out of object sharing on a per-connection basis. That is, you can mark a connection as being an "island", such that it won't share its objects to other connections, and it won't use shared objects from other connections. All objects that you get/set with this connection will remain only within this connection.

This is done via the "policy" settings, which can be configured independently for objects & metadata.

YapDatabaseConnection *connection = [database newConnection];

// Opting out of object sharing
connection.objectPolicy = YapDatabasePolicyContainment;
connection.metadataPolicy = YapDatabasePolicyContainment;

Please keep in mind that this is not a magic one-liner solution for problems you may be having. In particular, the very first example within this section demonstrated a thread-safety issue that involved only a single connection. Even with 'YapDatabasePolicyContainment', that thread-safety issue still exists.

Thus the recommended best practice, as stated further above, is to makes copies of database objects before modifying them. This recommendation is there for a reason:

  • it's straight-forward and easy to do
  • it allows all your database objects to be passed among threads
  • it allows your database objects to be shared between connections
  • it's enforceable using a few tricks

If you do choose to opt out of object sharing, I encourage you to do so on a temporary basis. Add a task to your bug tracking system to come back and tackle this issue properly. You'll get a little performance boost when you do. Not to mention enhanced thread-safety and peace of mind.

Clone this wiki locally