dispatch_block 崩溃

Crash with dispatch_block

我一直在努力了解这次崩溃背后的原因,以便更多地了解块的行为方式。我有一个非常简单的 class 来触发这次崩溃。

@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

现在,如果我分配 class 并简单地调用方法 doSomething 给它,

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

它崩溃了异常,EXC_BAD_INSTRUCTION 和以下堆栈跟踪,

#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 ()

如果我修改方法 doSomething,使其不使用 weak 而使用 self,则不会发生崩溃并且方法似乎按预期执行,

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

为什么会崩溃,我的理解是在块内部使用 weak 将确保不会调用该方法,如果对象被释放我认为 weak比在块内使用 self 更安全。

如果我保留对象 BlockCrashTest 并调用它的方法,上面的代码与 weakSelf 一起工作得很好。

如果有人能解释崩溃背后的原因以及这 3 种不同的代码变体到底发生了什么,一个崩溃和其他似乎工作正常,我将非常高兴。

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.

即使您删除 self performSomethingAsync: 的调用,您也会遇到同样的崩溃。此崩溃由 libdispatch 信号量 API 引起。您可以在 Xcode 中看到崩溃函数 _dispatch_semaphore_dispose 的汇编跟踪:

如果我们试图弄清楚这段代码中发生了什么,我们会看到您明确地标记当前块进入组呼叫 dispatch_group_enter。然后 performSomethingAsync 没有调用,因为 strongSelf == nil。这意味着 dispatch_group_enterdispatch_group_leave 不平衡,这导致该组无法正确处理并崩溃(参见 asm 清单)。

如果你使用 self 这段代码也会崩溃,因为 dispatch_group_leave(group); 从不同的线程调用 dispatch_group_enter 这也会导致同样的崩溃,但从另一个角度来看:调用不平衡同一个线程。 performSomethingAsync 在不同的队列中调用了完成块,而不是在你的队列中 "com.queue.test"

这个例子只是错误地使用了 dispatch_groups APIs。要了解如何正确使用它,请参阅 apple doc

一些观察:

  1. dispatch_group_t 对象被释放时,你不能有不平衡 "enter" 和 "leave" 的调度组。正如 ilya 指出的那样,由于你的模式,strongSelfnil,所以你进入了这个组,但没有离开它。

    weakSelfstrongSelf 舞蹈中一个非常常见的模式是只检查 strongSelf 是否是 nil,解决不平衡问题。因此,如果 strongSelfnil,它会完全绕过调度组的东西,但如果不是 nil,"enter" 和 "leave" 都会被调用:

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

    显然,您必须确保 performSomethingAsync 方法本身,总是 调用离开组的块。

  2. 解决此问题的另一种方法(如果您不能保证所有 "enter" 和 "leave" 将保持平衡),是使用信号量:

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

    坦率地说,即使像我上面那样使用信号量,我仍然认为人们会想要检查以确认 strongSelf 不是 nil。如果不将消息添加到导致无操作的 nil 对象的可能性,并发编程就已经足够混乱了。

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

MacOS 10.8 和 iOS6.0 引入了 ARC 来分派对象。来自文档 GCD Objects and Automatic Reference Counting:

When you build your app using the Objective-C compiler, all dispatch objects are Objective-C objects. As such, when automatic reference counting (ARC) is enabled, dispatch objects are retained and released automatically just like any other Objective-C object. When ARC is not enabled, use the dispatch_retain and dispatch_release functions (or Objective-C semantics) to retain and release your dispatch objects. You cannot use the Core Foundation retain/release functions.

If you need to use retain/release semantics in an ARC-enabled app with a later deployment target (for maintaining compatibility with existing code), you can disable Objective-C-based dispatch objects by adding -DOS_OBJECT_USE_OBJC=0 to your compiler flags.

在您的情况下,ARC 正在愉快地管理您的 dispatch_group_t 的生命周期。而且,不幸的是,您的代码导致组在锁仍在等待时被释放。当组超时时它被释放 - 所以当调用 dispatch_group_leave 时它崩溃,因为组已经被释放。

我建议至少在尝试离开之前检查该组是否为 NULL。

此外,您的等待结果逻辑是相反的。零结果表示该组在超时前被清空,非零结果表示超时。