__block不适合多线程并发

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则基本不会有什么太大的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述?设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型...
    龍飝阅读 2,133评论 0 12
  • 开始之前,我想先提几个问题,看看大家是否对此有疑惑。唐巧已经写过一篇对block很有研究的文章,大家可以去看看(本...
    高思阳阅读 1,602评论 0 1
  • D31 自由 我知道我是自由的,做自己想做的工作,爱自己想爱的人。 我知道我可以自由,无所谓别人的看法,没时间纠缠...
    碧舟阅读 612评论 0 49
  • 没关系,喜欢日子过的充实,心安
    念_385b阅读 336评论 2 0