Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Crash with dispatch_block

I have been trying to understand the reason behind this crash for the sake of understanding more about how blocks behave. I have a really simple class to trigger this crash.

@implementation BlockCrashTest

- (void)doSomething
{
    dispatch_queue_t queue = dispatch_queue_create("com.queue.test", DISPATCH_QUEUE_SERIAL);

    __weak typeof(self) weakSelf = self;

    dispatch_block_t block = ^{

        __strong typeof(weakSelf) strongSelf = weakSelf;

        dispatch_group_t group = dispatch_group_create();

        dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);

        dispatch_group_enter(group);
        [strongSelf performSomethingAsync:^{
            dispatch_group_leave(group);
        }];

        if(dispatch_group_wait(group, time) != 0) {
            NSLog(@"group already finished");
        }
    };
    dispatch_async(queue, block);
}

- (void)performSomethingAsync:(void(^)(void))completion
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(5);
        completion();
    });
}

- (void)dealloc
{
    NSLog(@"released object");
}

@end

Now, if I allocate the class and simply call method doSomething to it,

BlockCrashTest *someObject = [[BlockCrashTest alloc] init];
[someObject doSomething];

It crashes with the exception, EXC_BAD_INSTRUCTION and following stack traces,

#0  0x000000011201119a in _dispatch_semaphore_dispose ()
#1  0x0000000112013076 in _dispatch_dispose ()
#2  0x0000000112026172 in -[OS_dispatch_object _xref_dispose] ()
#3  0x000000010ef4c2fd in __29-[BlockCrashTest doSomething]_block_invoke at /Users/Sandeep/Desktop/Test Block Crash/Test Block Crash/ViewController.m:35
#4  0x0000000112005ef9 in _dispatch_call_block_and_release ()

If I modify the method doSomething, such that it do not use weak but uses self then the crash do not occur and the methods seem to execute as expected,

- (void)doSomething
{
    dispatch_queue_t queue = dispatch_queue_create("com.queue.test", DISPATCH_QUEUE_SERIAL);

    dispatch_block_t block = ^{

        dispatch_group_t group = dispatch_group_create();

        dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);

        dispatch_group_enter(group);
        [self performSomethingAsync:^{
            dispatch_group_leave(group);

        }];

        if(dispatch_group_wait(group, time) != 0) {
            NSLog(@"group already finished");
        }
    };
    dispatch_async(queue, block);
}

Why does it crash, my understanding is that using weak inside the block would make sure that the method would not be called, if the object is released and I thought that weak is safer than using self inside the block.

The above code works fine with weakSelf, if I retain the object BlockCrashTest and call the method to it.

I would be really happy if someone could explain the reason behind the crash and what exactly happens with these 3 different variants of code above that one crash and other seem to work fine.

Note: This is in one way or other related to the crash listed in thread, Objective-C crash on __destroy_helper_block_. I have been able to reproduce the exact same stack traces with my code above.

like image 363
Sandeep Avatar asked Oct 01 '15 08:10

Sandeep


Video Answer


2 Answers

A couple of observations:

  1. You cannot have a dispatch group with unbalanced "enter" and "leave" when the dispatch_group_t object is deallocated. And as ilya pointed out, because of your pattern, strongSelf is nil, so you're entering the group, but not leaving it.

    A very common pattern in the weakSelf and strongSelf dance is to just check to see if strongSelf was nil or not, resolving the imbalance. Thus, if strongSelf is nil, it bypasses the dispatch group stuff altogether, but if it is not nil, both "enter" and "leave" will be called:

    - (void)doSomething {
        dispatch_queue_t queue = dispatch_queue_create("com.queue.test", DISPATCH_QUEUE_SERIAL);
    
        typeof(self) weakSelf = self;
    
        dispatch_async(queue, ^{
            typeof(self) strongSelf = weakSelf;
    
            if (strongSelf) {
                dispatch_group_t group = dispatch_group_create();
    
                dispatch_group_enter(group);
                [strongSelf performSomethingAsync:^{
                    dispatch_group_leave(group);
                }];
    
                dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
            }
        });
    }
    

    Clearly, you must make sure that performSomethingAsync method, itself, always calls the block which leaves the group.

  2. The other way of solving this (if you don't have assurances that all of the "enter" and "leave" will be balanced), is to use semaphores:

    - (void)doSomething {
        dispatch_queue_t queue = dispatch_queue_create("com.queue.test", DISPATCH_QUEUE_SERIAL);
    
        typeof(self) weakSelf = self;
    
        dispatch_async(queue, ^{
            typeof(self) strongSelf = weakSelf;
    
            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
            [strongSelf performSomethingAsync:^{
                dispatch_semaphore_signal(semaphore);
            }];
    
            dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    
            if (dispatch_semaphore_wait(semaphore, time) != 0) {
                NSLog(@"semaphore not received in time");
            }
        });
    }
    

    Frankly, even when using semaphores, like I have above, I still think one would want to check to confirm that strongSelf was not nil. Concurrent programming is confusing enough without adding the possibility of the message to a nil object resulting in an no-op.

    - (void)doSomething {
        dispatch_queue_t queue = dispatch_queue_create("com.queue.test", DISPATCH_QUEUE_SERIAL);
    
        typeof(self) weakSelf = self;
    
        dispatch_async(queue, ^{
            typeof(self) strongSelf = weakSelf;
    
            if (strongSelf) {
                dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
                [strongSelf performSomethingAsync:^{
                    dispatch_semaphore_signal(semaphore);
                }];
    
                dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
    
                if (dispatch_semaphore_wait(semaphore, time) != 0) {
                    NSLog(@"semaphore not received in time");
                }
            }
        });
    }
    
like image 136
Rob Avatar answered Sep 29 '22 14:09

Rob


You'll get the same crash even if you remove call of self performSomethingAsync:. This crash caused by libdispatch semaphore API. You can see assembly trace of crashed function _dispatch_semaphore_dispose in Xcode: enter image description here

If we try to figure out what happens in this code we'll see that you explicitly mark current block entered the group by calling dispatch_group_enter. Then performSomethingAsync doesn't call because strongSelf == nil. Which means that dispatch_group_enter is not balanced with dispatch_group_leave this cause the group couldn't dispose properly and crashed (see asm listing).

If you use self this code also crashed because dispatch_group_leave(group); called from the different thread with dispatch_group_enter Which is also cause the same crash but in another perspective: calls not balanced in the same thread. performSomethingAsync called completion block in different queue not in yours "com.queue.test".

This example is just wrong using of dispatch_groups APIs. To see how properly use it see apple doc.

like image 23
Ilia Avatar answered Sep 29 '22 14:09

Ilia