iOS多线程之dispatch_once剖析

一,兴趣是最好的老师。

在IOS开发中,为保证单例在整个程序运行中只被初始化一次,单线程的时候,通过静态变量可以实现;但是多线程的出现,使得在极端条件下,单例也可能返回了不同的对象。如在单例初始化完成前,多个进程同时访问单例,那么这些进程可能都获得了不同的单例对象。

苹果提供了 dispatch_once(dispatch_once_t *predicate,dispatch_block_t block);函数来避免这个问题,作为程序员,应该都会对这个函数的实现方法很感兴趣。网上搜索了一下,有不少相关的帖子,看完我还是一头雾水。有帖子在说到这个问题时,还列出了汇编代码,然而这只是让我这不懂汇编的更加迷茫。然而,经过几个小时的搜索和思考,虽然没有弄明白dispatch_once函数是怎么实现的,但是总算把头文件里列出的内容弄清楚了。

多线程保护下的单例初始化代码:

+ (instancetype)defaultObject {
    static SharedObject *sharedObject = nil;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        sharedObject = [[SharedObject alloc] init];
    });
    return sharedObject;
}

通过查阅头文件,我们很容易知道 dispatch_once_t 就是 long 型,即长整型。静态变量在程序运行期间只被初始化一次,然后其在下一次被访问时,其值都是上次的值,其在除了这个初始化方法以外的任何地方都不能直接修改这两个变量的值。这是单例只被初始化一次的前提。

然后就是最神秘的 dispatch_once 函数了,如何才能保证,两个同时调用这个方法的进程,只执行一次这个函数的block块呢?看看头文件里,有没有什么说明。

首先定位到的是 :

#undef dispatch_once
#define dispatch_once _dispatch_once

这段代码的意思是,先取消 dispatch_once 的定义, 然后把 _dispatch_once 定义为 dispatch_once。这是在干什么? 在定位一下 _dispatch_once 这个函数,就在这句话的上面,完整的一段代码贴出来。

#ifdef __BLOCKS__
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
void
dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
void
_dispatch_once(dispatch_once_t *predicate, dispatch_block_t block)
{
    if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
        dispatch_once(predicate, block);
    }
}
#undef dispatch_once
#define dispatch_once _dispatch_once
#endif

对于宏定义不熟悉的同学来说,这一堆宏定义,简直根乱码一样。上面这段代码的意思大概是,先声明了 dispatch_once 函数,下面又实现了 _dispatch_once 函数。所以上面那个先取消又定义的宏,应该是:用户调用  dispatch_once 函数,实际上调用的是 _dispatch_once 函数;而真正的 dispatch_once 函数是在 _dispatch_once 内调用的。绕个圈这么做,是为什么 ?继续分析。

通过分析 _dispatch_once 函数,除了 DISPATCH_EXPECT 这个方法外,别的都很正常,那么就看下这个东西是个啥。(完全新手可能不懂 ~0l 是啥,这个意思长整型0按位取反,其实就是长整型的-1)。

#if __GNUC__
#define DISPATCH_EXPECT(x, v) __builtin_expect((x), (v))
#else
#define DISPATCH_EXPECT(x, v) (x)
#endif

__GNUC__是啥?好像是编译器相关的,不作深究。#define DISPATCH_EXPECT(x, v) (x) 但是这个的意思很明显,就是如果没有定义__GNUC__的话 DISPATCH_EXPECT(x, v)  就是第一个参数 (x)。

百度一下后得知,对于 __builtin_expect ,就是告诉编译器,它的第一个参数的值,在very very very很大的情况下,都会是第二个参数。
好吧,现在回到 _dispatch_once 函数,再看它的意思: DISPATCH_EXPECT(*predicate, ~0l)  就是说,*predicate 很可能是 ~0l ,而当  DISPATCH_EXPECT(*predicate, ~0l)  不是 ~0! 时 才调用真正的 dispatch_once 函数。

这是啥情况,这段代码好像根如何保证代码只被执行一次并没有什么关系!细分析之,第一次运行,predicate的值是默认值0,按照逻辑,如果有两个进程同时运行到 dispatch_once 方法时,这个两个进程获取到的 predicate 值都是0,那么最终两个进程都会调用 最原始那个 dispatch_once 函数!!!

代码绕来绕去,研究半天,竟然最后还是会同时调用 dispatch_once 函数,既然如此,上面哪些代码意义何在?!

网上看了不少帖子,关于多线程保护的逻辑,都只是分析了 _dispatch_once 函数,然而这并没有用。

在我看来,头文件里列出的内容,并不是 dispatch_once 实现多线程保护的逻辑,而是编译优化逻辑。也就是告诉编译器,在调用 dispatch_once 时,绝大部分情况不用调用原始的 dispatch_once ,而是直接运行后续的内容。

所以真正的实现的多线程保护逻辑,苹果并没有展示给我们,封装在原始的 dispatch_once 函数的实现里,里面应该有关于进程锁类似的机制,保证某段代码在即使有多个线程同时访问时,只有一个线程被执行。既然真正的逻辑并没有展示,那就没有深究下去了,苹果说这个函数是只能被执行一次,我们使用就是了。

那么在这里,我其实也可以猜测,predicate的数值,肯定在block运行后被更改为 ~0l ,即 -1,可以用下面的代码测试一下。

+ (instancetype)defaultObject{
    static SharedObject *sharedObject = nil;
    static dispatch_once_t predicate;
    NSLog(@"在dispatch_once前:%ld", predicate);
    dispatch_once(&predicate, ^{
	 NSLog(@"在dispatch_once中:%ld", predicate);
         sharedObject = [[SharedObject alloc] init]; 
    });
    NSLog(@"在dispatch_once后:%ld", predicate);
    return sharedObject;
}


执行结果:

在dispatch_once前:0
在dispatch_once中:140734607350288
在dispatch_once后:-1

二、dispatch_once是否真的线程安全?

非常极端的情况下,dispatch_once会发生死锁,例如下面这个例子,设计目的是两个单例互相依赖,一个初始化了,另一个也要初始化。

@interface TestA : NSObject
+ (TestA *)sharedInstance;
@end

@interface TestB : NSObject
+ (TestB *)sharedInstance;
@end

@implementation TestA

+ (TestA *)sharedInstance {
    static TestA *testA = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        testA = [[TestA alloc] init];
        [TestB shareInstanceB];
    });
    return testA;
}

@end

@implementation TestB

+ (TestB *)sharedInstance {
    static TestB *testB = nil;
    static dispatch_once_t token;
    dispatch_once(&token, ^{
        testB = [[TestB alloc] init];
        [TestA shareInstanceA];
    });
    return testB;
}

@end

当获取任何一个单例的时候,都会造成程序卡死。实际上,任何造成线程进入了互相等待状态的操作,都会导致线程死锁,上面的两个互相依赖的单例只是一个特例。在规范的业务员设计中要避免上面情况,但是如果业务中真的需要这样的设计,那么可以使用锁来解决。

#import <objc/objc-sync.h>

@implementation TestA

+ (TestA *)sharedInstance {
    static TestA *_shared = nil;
    if (_shared != nil) {
        return _shared;
    }
    objc_sync_enter(self);
    if (_shared != nil) {
        objc_sync_exit(self);
        return _shared;
    }
    _shared = [[TestA alloc] init];
    [TestB sharedInstance];
    objc_sync_exit(self);
    return _shared;
}

@end

@implementation TestB

+ (TestB *)sharedInstance {
    static TestB *_shared = nil;
    if (_shared != nil) {
        return _shared;
    }
    objc_sync_enter(self);
    if (_shared != nil) {
        objc_sync_exit(self);
        return _shared;
    }
    _shared = [[TestB alloc] init];
    [TestA sharedInstance];
    objc_sync_exit(self);
    return _shared;
}

@end

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值