Henson Fang
Henson Fang

Reputation: 1207

Why modifying mutable object in multi-threading environment will cause EXC_BAD_ACCESS?

My properties:

@property (nonatomic, strong) NSMutableData *data1;
@property (nonatomic, strong) NSData *data2;

When I modify data1, as NSMutableData in multi-threading, it does crash. When I modify data2, as NSData in multi-threading, it does not crash.

for (int i = 0; i < 1000; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        self.data1 = [[NSMutableData alloc] init]; // crashes
        self.data2 = [[NSData alloc] init];        // does not crash
    });
}

Why? What's the difference?

Upvotes: 0

Views: 170

Answers (1)

zrzka
zrzka

Reputation: 21259

Property setter

@property (nonatomic, strong) NSMutableData *data;

MRC

We had something called MRC before the ARC. MRC stands for Manual Reference Counting and ARC stands for Automatic Reference Counting. In these times, one can implement setter in this way:

- (void)setData:(NSMutableData *)data {
    id oldValue = _data;
    if (oldValue == data) {
        return;
    }
    _data = [data retain];
    [oldValue release];
}

ARC

In ARC, it's way simpler:

- (void)setData:(NSMutableData *)data {
    _data = data;
}

Why? Because the compiler automatically inserts retain & release calls for you. But it's important to know that these calls are still there.

objc_storeStrong

Put this property in any of your class and select Product - Perform Action - Assemble "YourClass.m" from the Xcode menu. You'll get generated assembly of the file (mine is in the AppDelegate):

"-[AppDelegate setData:]":              ## -- Begin function -[AppDelegate setData:]
                                        ## @"\01-[AppDelegate setData:]"
Lfunc_begin5:
    .loc    1 13 0                  ## MRC/AppDelegate.m:13:0
    .cfi_startproc
## %bb.0:
    ##DEBUG_VALUE: -[AppDelegate setData:]:self <- $rdi
    ##DEBUG_VALUE: -[AppDelegate setData:]:self <- $rdi
    ##DEBUG_VALUE: -[AppDelegate setData:]:_cmd <- $rsi
    ##DEBUG_VALUE: -[AppDelegate setData:]:data <- $rdx
    ##DEBUG_VALUE: -[AppDelegate setData:]:data <- $rdx
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
Ltmp12:
    .loc    1 0 0 prologue_end      ## MRC/AppDelegate.m:0:0
    addq    $16, %rdi
Ltmp13:
    ##DEBUG_VALUE: -[AppDelegate setData:]:self <- [DW_OP_LLVM_entry_value 1] $rdi
    movq    %rdx, %rsi
Ltmp14:
    ##DEBUG_VALUE: -[AppDelegate setData:]:_cmd <- [DW_OP_LLVM_entry_value 1] $rsi
    ##DEBUG_VALUE: -[AppDelegate setData:]:data <- $rsi
    popq    %rbp
Ltmp15:
    jmp _objc_storeStrong       ## TAILCALL
Ltmp16:
Lfunc_end5:
    .cfi_endproc

Even if you can't read assembly, don't worry, you'll see that there's not much going on and that there's the objc_storeStrong function call. The source code looks like:

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

What it does?

  • Stores the current object pointer in the prev variable
  • Compares pointers (current and new object) and returns early if they do equal
  • Retains new object
  • Replaces the current object with already retained new object
  • Finally releases the old object

As you can see there's a lot of stuff going on between the id prev = *location and objc_release(prev) lines.

Crash

Back to your crash. The crash you see is something like this:

(lldb) bt
* thread #6, queue = 'com.apple.root.default-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0x20d71fa95490)
    frame #0: 0x00007fff6d15130f libobjc.A.dylib`objc_release + 31
    frame #1: 0x00000001069c821e MRC`-[AppDelegate setData1:](self=0x0000600001472660, _cmd="setData1:", data1=0 bytes) at AppDelegate.m:0    

objc_release is crashing. Why? Scroll up and check the objc_storeStrong function implementation again. Remember how many things it does between the id prev = *location and objc_release(prev) lines?

And now imagine that in your multi-threaded environment, you call objc_storeStrong thousand times, before the first call finishes, another is started, stores the same pointer in the prev variable and then it is going to release the same object for 2nd, 3rd, 4th, ... time.

NSData

Okay, I understand, but why NSData property doesn't crash? Well, NSData is immutable and when you just allocate & initialize it, you'll actually get shared _NSZeroData - NSData subclass used for zero length NSData.

You can check it in a simple way:

NSData *d1 = [[NSData alloc] init];
NSData *d2 = [[NSData alloc] init];
NSData *d3 = [[NSData alloc] init];
NSData *d4 = [[NSData alloc] init];

NSLog(@"%p %p %p %p", d1, d2, d3, d4);

Output on my computer is:

0x60000001cd10 0x60000001cd10 0x60000001cd10 0x60000001cd10

%p prints pointer (memory address) and all d1, d2, d3 & d4 contains pointer to the same object.

Back to the objc_storeStrong implementation:

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

Pointers do equal in this case hence the early return. In other words, this function does nothing in this specific case.

Start with the Threading Programming Guide. Learn more about atomic vs nonoatomic, (not-)thread-safe data types, @synchronized and other primitives you can use in multi-threaded environment.

Upvotes: 2

Related Questions