bugsOC 底层

__block不适合多线程并发

2019-04-24  本文已影响0人  Colla

objc的很多设计,从底层实现上都不完全是线程安全的,这也导致在一些极端的并发情况下,会引起竞争导致的内存访问错误问题。之前分析过_weak的设计不是多线程安全的,最近又踩坑了_block,发现这个居然也不是线程安全。

当然这也不是说 _block, _weak 这些不要用了,而是说在比较频繁创建释放且有多线程使用的情况下,不要用 ___block, _weak修饰,因为他们的确不是线程安全的。

关于__weak的问题原文 不安全的weak

一、问题

最近线上新版本发布后,由于框架大改版导致一个以前几乎没有暴露出来的crash突然暴露出来了,虽然总量和频率也不高,但是每天也能有几十次,这个crash还有很有特色的。
crash堆栈如下:

Exception Type: SIGTRAP
Exception Codes: 0 at 0x000000019cfb6e8c
Crashed Thread: 11

Thread 11 Crashed: 
0  libsystem_blocks.dylib         0x000000019cfb6e8c _Block_object_dispose + 284
1  mttlite                        0x0000000104bbe114 -[MttSpaceClearManager extractSimilarImages:inOperation:] (MttSpaceClearManager.mm:611)
2  mttlite                        0x0000000104bbcc04 -[MttSpaceClearManager getSimilarImagesWithDataSource:inOperation:] (MttSpaceClearManager.mm:0)
3  mttlite                        0x0000000104bbc6e0 __66-[MttSpaceClearManager getSimilarImagesWithDataSource:completion:]_block_invoke (MttSpaceClearManager.mm:0)
4  Foundation                     0x000000019dfae82c ___NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ +  16
 +  16
5  Foundation                     0x000000019deb6a28 -[NSBlockOperation main] +  72
6  Foundation                     0x000000019deb5efc -[__NSOperationInternal _start:] +  740
7  Foundation                     0x000000019dfb0700 ___NSOQSchedule_f +  272
8  libdispatch.dylib              0x000000019cf596c8 __dispatch_call_block_and_release +  24
9  libdispatch.dylib              0x000000019cf5a484 __dispatch_client_callout +  16
10 libdispatch.dylib              0x000000019cf30e14 __dispatch_continuation_pop$VARIANT$armv81 +  404
11 libdispatch.dylib              0x000000019cf304f8 __dispatch_async_redirect_invoke +  592
12 libdispatch.dylib              0x000000019cf3cafc __dispatch_root_queue_drain +  344
13 libdispatch.dylib              0x000000019cf3d35c __dispatch_worker_thread2 +  116
14 libsystem_pthread.dylib        0x000000019d13c17c _pthread_wqthread + 460
1  libsystem_pthread.dylib        0x000000019d13ecec _start_wqthread +  4

Binary Images:
0x10230c000 - 0x105713fff +mttlite arm64 <336143a1676d3cc18550136117ee2a60> /var/containers/Bundle/Application/AB7143CF-079B-49FA-99E9-AAE55D78491A/mttlite.app/mttlite
0x19cfb6000 - 0x19cfb6fff  libsystem_blocks.dylib arm64 <3e987452dc8b3ad9ab242066ddb75743> /usr/lib/system/libsystem_blocks.dylib

对应实际代码大概如下

__block NSMutableArray *images = [NSMutableArray array]; 

 //并发遍历数组groups
    [groups enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSArray * _Nonnull group, NSUInteger idx, BOOL * _Nonnull stop) {
        //各种dispatch_async再到其它线程,同时也捕获使用了这个__block的images数组。
        
        [images addObject:xxx];
}

而crash就发生在enumerateObjectsWithOptions:执行完后,一直会报SIGTRAP的问题。

二、分析问题

SIGTRAP

SIGTAP类问题一般都是系统主动触发brk指令,说明有逻辑或数据异常,系统抛异常了。
crash发生在_Block_object_dispose 地址0x000000019cfb6e8c ,代码和分析结果如下图:

请在这里填写图片描述

_Block_object_dispose函数如果走入SIGTRAP逻辑,则必定是其引用计数出现问题了,那到底怎么出现了问题,我们需要结合lib_closure源码以及反汇编来分析一下。

首先根据crash地址找到crash地址0x0000000104bbe114 在我们app二进制的实际位置,计算如下:

相对macho偏移地址=0x0000000104bbe114-0x10230c000;
macho静态地址=0x0100000000 + 相对macho偏移地址;

根据macho静态地址,打开Hopper得到如下结果:

请在这里填写图片描述

就是在调用_Block_object_dispose方法的时候发生了crash,x0传入的时该__block内存,x1比较特殊等于8,

分析源码

这里我们打开lib_closure的源码查看Block_object_dispose大概干了什么。

//https://opensource.apple.com/source/libclosure/libclosure-53/
void _Block_object_dispose(const void *object, const int flags) {
    switch (osx_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_destroy(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

enum {
    // see function implementation for a more complete description of these fields and combinations
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

根据源码推测x1=8代表这是一个__block对象,也就是这里是在执行一个__block对象的释放,释放时发现其引用计数<=0,从而触发了异常。

__block内存结构

一个__block声明的对象/数据,编译后就不再是一个简单的栈上的内存数据了,而会变成一个堆上的数据,其数据结构大概如下

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int flags; // contains ref count
    unsigned int size;
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
    // long shared[0];
};

此处以__block NSMutableArray *images为例,其编译后,大概内存结构如下:

struct Block_byref_xxx {
    void *isa;
    struct Block_byref *forwarding;
    volatile int flags; // contains ref count
    unsigned int size;
    void *images;//这个位置是数据images指针
};

__block引用计数管理

__block对象引用计数管理大概规则如下:

当__block对象被block捕获使用后,第一次时会执行一次拷贝,把内存从栈上拷贝到堆上,并将引用计数增加+2(__block比较特殊,引用计数增加一次是+2,而不是+1,源码上是这么写的),其对应的flags的第1到11位存储了其引用计数值;当执行第一次从栈到堆的拷贝时引用计数值+4(即引用计数为2),其后每次拷贝/retain,则调用_Block_byref_assign_copy函数触发引用计数值+2,当release时调用_Block_byref_release触发引用计数-2,直到<=0则触发内存释放(前提是这个是堆内存);

这里__block对象的引用计数不像ARC中NSObject一样有一个SideTables并配合加锁解锁来辅助存储每个指针对应的引用计数,而是由其自身内存flags字段存储了引用计数,本来这也没有什么大的问题,但是在这里其操作flags字段的接口并不完全是线程安全的,导致其可能flags值修改不一致。源码如下:

static void _Block_byref_release(const void *arg) {
    struct Block_byref *shared_struct = (struct Block_byref *)arg;
    unsigned int refcount;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    shared_struct = shared_struct->forwarding;
    
    // To support C++ destructors under GC we arrange for there to be a finalizer for this
    // by using an isa that directs the code to a finalizer that calls the byref_destroy method.
    if ((shared_struct->flags & BLOCK_NEEDS_FREE) == 0) {
        return; // stack or GC or global
    }
    refcount = shared_struct->flags & BLOCK_REFCOUNT_MASK;
    osx_assert(refcount);
    if (latching_decr_int_should_deallocate(&shared_struct->flags)) {
        if (shared_struct->flags & BLOCK_HAS_COPY_DISPOSE) {
            (*shared_struct->byref_destroy)(shared_struct);
        }
        _Block_deallocator((struct Block_layout *)shared_struct);
    }
}

//__block设计上是使用latching_decr_int_should_deallocate基于原子交互来确保数据竞争安全问题
//引用计数-- release
static bool latching_decr_int_should_deallocate(volatile int *where) {
    while (1) {
        int old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return false; // latched high
        }
        if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
            return false;   // underflow, latch low
        }
        int new_value = old_value - 2;
        bool result = false;
        if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
            new_value = old_value - 1;
            result = true;
        }
        if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
            return result;
        }
    }
}

//引用计数++ retain
static int latching_incr_int(volatile int *where) {
    while (1) {
        int old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

通过源码查看,我们发现其对应的相关release和reatin方法里会频繁直接访问或修改flags字段,而未完全加锁,虽然在某些关键修改处它使用了原子交互来解决线程竞争的问题,但从时机真机表现来看,iOS系统上带的libsystem_blocks.dylib库依旧会出现极个别并发情况下,flags引用计数竞争的问题。这时就可能会导致引用计数管理出错,从而触发其异常逻辑;

结论

综上,我们发现 ___block的引用计数管理在iOS上并不是完全的线程安全的,导致当一个___block对象频繁多线程并发retain,release后,其引用计数flags由于数据竞争导致出错,从而触发libblocks的异常。所以___block修饰符,并不适合有大量多线程并发场景下使用;但一般情况下继续使用___block则基本不会有什么太大的问题。

上一篇下一篇

猜你喜欢

热点阅读