Skip to content
Robbie Hanson edited this page Nov 9, 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 for 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 (you might like yours better), but rather because its one that tends to be 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 in mind that, although YapDatabase is thread-safe, the objects you're fetching from the database may not be. 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];
    }];
}

Looking at such a simple example, the mistake seems pretty obvious. But since YapDatabaseConnection is thread-safe, it can be easy to forget that thread-safety in one place doesn't extend to everything else.

But are we referring to the exact same person object? Or is each a different copy?

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 when your objects are mutable.

If fact, if you follow this simple guideline, YapDatabase has additional performance optimizations you can enable to speed up your database. See the Performance Pro article for additional details.

Clone this wiki locally