Luca Bernardi
Luca Bernardi

Reputation: 4199

Different behaviour between enumerateObjectsUsingBlock: and for( in )

Give this code

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(@"----");
});

I'm expecting that this code crash because at some point the block dispatched on queue2 get executed concurrently to the enumeration and this will trigger the assertion that you cannot mutate an the array while enumerating. Indeed, this is what happens.

The interesting part is when you substitute for ( in ) with 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(@"----");
});

In all my different test the block that remove the object is executed in the middle of the enumeration (I see the print of @"----") and the interesting thing is that the enumeration behave correctly printing [8999] 8999 and then [9000] 9001.

In this case the array is mutated during the enumeration without firing any assertion. Is this an intended behaviour? If yes, why? I'm I missing something?

Upvotes: 4

Views: 575

Answers (1)

AndrewS
AndrewS

Reputation: 8472

Since the introduction of fast enumeration, it's become the go-to method for ... making enumeration fast. Most implementations of enumerations, such as for(in) and enumerateObjectsUsingBlock:, will use fast enumeration under the covers.

Fast enumeration will look at how the data is stored. In the case of NSMutableArray, I would guess that the underlying data is stored in several chunks of data; a ten-thousand item array might be implemented as a hundred chunks of 100 items, with each chunk storing its hundred items in contiguous memory. Analysis of some assembly suggests that (at least on some iOS devices) the class is implemented as a single, giant circular buffer. Either way, the enumerated list might contain multiple contiguous blocks of objects. Ultimately, the exact storage mechanism is irrelevant; it's access to the underlying contiguous storage that makes fast enumeration better than the alternative.

Generally, enumeration is supposed to prevent the list from being mutated. You will always see that with the for(in) enumeration. Apparently some implementations of enumerateObjectsUsingBlock: do not robustly guarantee that the list is not mutated during enumeration. I am getting an assertion failure on the devices that I've tried... but it sounds like there are some devices where this protection is broken. I'd guess that the mutation guard used within NSFastEnumerationState is not complete, perhaps only watching a single chunk instead of the entire array.

I'd consider this a bug in enumerateObjectsUsingBlock:.

Further, any code that might generate an exception here is by definition bad code: you'll need to provide a mechanism to prevent your own code from trying to modify an array while another thread is iterating over it.

Upvotes: 0

Related Questions