aednichols
aednichols

Reputation: 2322

Non-deterministic crash in BlocksKit bk_apply block

I have a function that constructs an NSMutableDictionary using bk_apply, a method provided by the third-party block utility library BlocksKit. The function's test suite usually passes just fine, but once every couple runs it crashes.

NSMutableDictionary *result = [[NSMutableDictionary alloc] init];

[inputSet bk_apply:^(NSString *property) {
    NSString *localValueName = propertyToLocalName[property];
    NSObject *localValue = [self valueForKey:localValueName];

    result[property] = localValue ?: defaults[property];                // Crash

    // Convert all dates in result to ISO 8601 strings
    if ([result[property] isKindOfClass:[NSDate class]]) {              // Crash
        result[property] = ((NSDate *)result[property]).ISODateString;  // Crash
    }

}];

The crash always happens on a line where result is referenced, but it's not the same line every time.

Examining the contents of result in the debugger, I've seen very strange values like

po result
{
    val1 = "Some reasonable value";
    val2 = "Also reasonable value";
    (null) = (null);
}

It's impossible for an NSDictionary to have null keys or values, so clearly some invariant is being violated.

What is causing this crash and how do I fix it?

Upvotes: 1

Views: 66

Answers (1)

aednichols
aednichols

Reputation: 2322

From the BlocksKit documentation for bk_apply:

Enumeration will occur on appropriate background queues. This will have a noticeable speed increase, especially on dual-core devices, but you must be aware of the thread safety of the objects you message from within the block.

The code above is highly unsafe with respect to threading, because it reads from and writes to a mutable variable on multiple threads.

The intermittent nature of the crash comes from the fact that the thread scheduler is non-deterministic. The crash won't happen when several threads accessing shared memory happen to have their execution scheduled in sequence rather than in parallel. It is therefore possible to "get lucky" some or even most of the time, but the code is still wrong.


The debugger printout is a good example of the danger. The thread that's paused is most likely reading from result while another thread performs an insertion.

NSMutableDictionary insertions are likely not atomic; example steps might be,

  1. allocate memory for the new entry
  2. copy the entry's key into the memory
  3. copy the entry's value into the memory

If you read the dictionary from another thread between steps 1 and 2, you will see an entry for which memory has been allocated, but the memory contains no values.


The simplest fix is to switch to bk_each. bk_each does the same thing as bk_apply but it's implemented in a way that guarantees sequential execution.

Upvotes: 2

Related Questions