BlocksKit bk_apply 块中的非确定性崩溃

Non-deterministic crash in BlocksKit bk_apply block

我有一个使用 bk_apply, a method provided by the third-party block utility library BlocksKit 构造 NSMutableDictionary 的函数。该函数的测试套件通常通过得很好,但一旦每对运行一次,它就会崩溃。

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
    }

}];

崩溃总是发生在引用 result 的行上,但每次都不是同一行。

在调试器中检查 result 的内容,我看到了非常奇怪的值,例如

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

NSDictionary 不可能有 null 键或值,因此显然违反了某些不变量。

导致此崩溃的原因是什么,我该如何解决?

来自 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.

上面的代码在线程方面非常不安全,因为它在多个线程上读取和写入可变变量。

崩溃的间歇性是因为 thread scheduler 是不确定的。当访问共享内存的多个线程碰巧按顺序而不是并行安排它们的执行时,不会发生崩溃。因此有可能 "get lucky" 一些甚至大部分时间,但代码仍然是错误的。


调试器打印输出是危险的一个很好的例子。暂停的线程最有可能从 result 读取,而另一个线程执行插入。

NSMutableDictionary 插入可能不是原子的;示例步骤可能是,

  1. 为新条目分配内存
  2. 将条目的密钥复制到内存中
  3. 将条目的值复制到内存中

如果您在步骤 12 之间从另一个线程读取字典,您将看到一个已分配内存的条目, 但内存不包含任何值。


最简单的解决方法是切换到 bk_eachbk_eachbk_apply 做同样的事情,但它是以保证顺序执行的方式实现的。