【问题标题】:Different behaviour between enumerateObjectsUsingBlock: and for( in )enumerateObjectsUsingBlock: 和 for( in ) 之间的不同行为
【发布时间】:2014-04-25 15:19:33
【问题描述】:

给出这个代码

NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
    [array addObject:@(i)];
}

queue1 = dispatch_queue_create("com.test_enumaration.1", DISPATCH_QUEUE_CONCURRENT);
queue2 = dispatch_queue_create("com.test_enumaration.2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    int idx = 0;
    for (NSNumber *obj in array) {
        NSLog(@"[%d] %@", idx, obj);
        idx++;
    }
});

double delayInSeconds = 0.3;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue2, ^(void){
    [array removeObjectAtIndex:9000];
    NSLog(@"----");
});

我预计这段代码会崩溃,因为在某个时候,在 queue2 上调度的块会与枚举同时执行,这将触发您无法在枚举时改变数组的断言。确实,这就是发生的事情。

有趣的部分是当您将for ( in ) 替换为enumerateObjectsUsingBlock:

NSMutableArray *array = [NSMutableArray new];
for (int i = 0; i < 10000; i++) {
    [array addObject:@(i)];
}

queue1 = dispatch_queue_create("com.test_enumaration.1", DISPATCH_QUEUE_CONCURRENT);
queue2 = dispatch_queue_create("com.test_enumaration.2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue1, ^{
    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"[%d] %@",idx, obj);
    }];
});

double delayInSeconds = 0.3;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue2, ^(void){
    [array removeObjectAtIndex:9000];
    NSLog(@"----");
});

在我所有不同的测试中,删除对象的块在枚举中间执行(我看到了@“----”的打印),有趣的是枚举行为正确打印[8999] 8999然后[9000] 9001

在这种情况下,数组在枚举过程中发生突变,而不触发任何断言。这是预期的行为吗?如果是,为什么?我错过了什么?

【问题讨论】:

  • @trojanfoe 不是真的,在链接的答案中说“这两种方法都可以保护可变集合免受枚举循环内的突变”在我的情况下,我并没有尝试更改枚举块内的集合但我正在从另一个线程改变它。
  • 我再次运行测试,对我来说知道崩溃,我看到在枚举中间记录了“----”,实际上输出是 [8999] 8999 和 [ 9000] 9001
  • 我得到了与@LucaBernardi 描述的相同的输出。实际上,似乎 enumerateObjectsUsingBlock: 在枚举时数组发生突变并简单地跳过删除的对象时不会抱怨。

标签: objective-c multithreading nsmutablearray nsarray enumeration


【解决方案1】:

自从引入快速枚举以来,它已成为...使枚举快速的首选方法。大多数枚举的实现,例如for(in)enumerateObjectsUsingBlock:,都会在后台使用快速枚举。

快速枚举将查看数据的存储方式。在 NSMutableArray 的情况下,我猜底层数据存储在几个数据块中;一万个项目数组可以实现为 100 个项目的一百个块,每个块将其一百个项目存储在连续的内存中。对某些程序集的分析表明(至少在某些 iOS 设备上)该类被实现为单个巨大的循环缓冲区。无论哪种方式,枚举列表都可能包含 多个 连续的对象块。最终,确切的存储机制是无关紧要的; 访问底层的连续存储使得快速枚举比其他方法更好。

一般来说,枚举应该防止列表被变异。您将始终通过for(in) 枚举看到这一点。显然,enumerateObjectsUsingBlock: 的某些实现不能可靠地保证列表在枚举期间不会发生突变。我在我尝试过的设备上遇到断言失败......但听起来有些设备的这种保护被破坏了。我猜NSFastEnumerationState 中使用的突变保护不完整,可能只看一个块而不是整个数组。

我认为这是enumerateObjectsUsingBlock: 中的一个错误。

此外,任何可能在此处生成异常的代码都被定义为错误代码:您需要提供一种机制来防止您自己的代码在另一个线程正在迭代时尝试修改数组在它上面。

【讨论】:

  • 您是否有任何资源可以证明 NSMutableArray 将其数据存储在块中? This article 表明它是一个循环缓冲区。
  • Mike Ash 暗示mikeash.com/pyblog/… 就是这种情况。 Bartosz,在您提到的文章中,同样对实际实现进行了猜测。除此之外,实现本身不是开源的,Apple 保留在不同设备和操作系统版本上更改实现的能力。所以,不,我不知道它是这样存储的。我将编辑帖子以反映这一点。
  • Mike Ash 的文章没有对 NSMutableArray 的内部做任何陈述。
  • 确实没有说法;只是一个暗示。另外,具体的实现是无关紧要的。重点是向读者介绍“多个连续对象块”的概念,我认为这可能是该错误的来源。
  • 以及含义在哪里。我找不到它了。如果一个数组中有多个块,Bartosz 的大部分观察都是不可能的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-14
  • 2014-11-01
  • 1970-01-01
  • 2018-05-07
相关资源
最近更新 更多