细说@synchronized和dispatch_once

工欲善其事,必先利其器

通常我们在实现单例时候都会使用synchronized或者dispatch_once方法,初始化往往是下面的样子:
使用synchronized方法实现:

static id obj = nil;
+(instancetype)shareInstance
{
    @synchronized(self) {
        if (!obj) {
            obj = [[SingletonObj alloc] init];
        }
    }
    return obj;
}

使用dispatch_once方法实现:

static id obj = nil;
+(instancetype)shareInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        obj = [[SingletonObj alloc] init];
    });
    return obj;
}
性能差异

上面的这些写法大家应该都很熟悉,既然两种方式都能实现,我们来看看两者的性能差异,这里简单写了个测试的demo,使用两个方法分单线程跟多线程(采用dispatch_apply方式,性能相对较高)去访问一个单例对象一百万次,对比这期间的耗时,从iPod跟5s测试得到如下的结果

    //ipod,主线程
    SingletonTest[4285:446820] synchronized time cost:2.202945s
    SingletonTest[4285:446820] dispatch_once time cost:0.761034s
    
    //5s,主线程
    SingletonTest[5372:2394430] synchronized time cost:0.466293s
    SingletonTest[5372:2394430] dispatch_once time cost:0.070822s

    //ipod,多线程
    SingletonTest[4315:448499] synchronized time cost:3.385109s
    SingletonTest[4315:448499] dispatch_once time cost:0.908009s
    
    //5s,多线程
    SingletonTest[5391:2399069] synchronized time cost:0.507504s
    SingletonTest[5391:2399069] dispatch_once time cost:0.169934s

可以发现dispatch_once方法的性能要明显优于synchronized方法(多线程不采用dispathc_apply方式差距更明显),所以在实际的应用中我们可以多采用dispatch_once方式来实现单例。通常使用的时候了解这些就够了,不过想知道两者的具体差异就需要我们再迈进一步。

深入@synchronized(object)

翻看苹果的文档可以发现 @synchronized指令内部使用锁来实现多线程的安全访问,并且隐式添加了一个异常处理的handler,当异常发生时会自动释放锁。在stackoverflow上看到@synchronized指令其实可以转换成objc_sync_enter跟objc_sync_exit,可以在<objc/objc-sync.h>头文件中找到这两个函数:

//Allocates recursive pthread_mutex associated with 'obj' if needed
int objc_sync_enter(id obj)

//End synchronizing on 'obj'
int objc_sync_exit(id obj)

根据注释文档,objc_sync_enter会根据需要给每个传进来的对象创建一个互斥锁并lock,然后objc_sync_exit的时候unlock,这样就可以通过这个锁来实现多线程的安全访问,所以结合苹果文档可以认为

@synchronized(self) {
    //thread safe code
}

等价于

@try {
    objc_sync_enter(self);
    // thread safe code
} @finally {
    objc_sync_exit(self);    
}

庆幸的是苹果已经将objc-runtime这部分开源,所以我们可以更进一步了解内部的实现,源码在这里,有兴趣也可以自己去查阅,这里简单介绍一下。
让我们先来看看几个数据结构,其中有些涉及到缓存,我们就不去考虑了:

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

struct SyncList {
    SyncData *data;
    spinlock_t lock;
};

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

首先看看SyncData这个数据结构,包含一个指向object的指针,这个object对象就是我们@synchronized时传进来的对象,也包含一个跟object关联的递归互斥锁recursive_mutex_t,该锁用来互斥访问object对象;同时还包含一个指向下一个SyncData的指针nextData,可以看出SyncData是一个链表中的节点;至于threadCount,这个值标示有几个线程正在访问这个对象,当threadCount==0的时候,会重用该SyncData对象,这是为了节省内存。
  接下来看看SyncList,SyncList其实就是一个链表,data指向第一个SyncData节点,lock则是为了多线程安全访问该链表。
  最后看下sDataLists静态哈希表对象,它以obj的指针为key,对应的value为SyncList链表。
  了解上面之后,我们就可以看看objc_sync_enter跟objc_sync_exit的具体实现(摘取部分代码)


//根据object对象去查询相应的SyncData对象,如果没有则创建一个新的
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    //lock,多线程安全访问SyncList
    lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            //找到object对象对应的SyncData对象,增加其threadCount计数,然后返回
            if ( p->object == object ) {
                result = p;
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //当threadCount == 0时,设置当前SyncData为可重用
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
        // 如果有可重用的节点,则使用当前SyncData节点,SyncData的object指针指向新的object对象
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    //如果没有可重用的节点,则创建一个新的SyncData节点
    result = (SyncData*)calloc(sizeof(SyncData), 1);

    //将新的SyncData节点的object指针指向传进来的object对象
    result->object = (objc_object *)object;
    result->threadCount = 1;

    //创建一个新的与该object关联的递归互斥锁
    new (&result->mutex) recursive_mutex_t();
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    return result;
}

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        //根据obj指针的哈希值查找对应的SyncData,threadcount计数加一
        SyncData* data = id2data(obj, ACQUIRE);

        //使用SyncData的互斥锁上锁
        data->mutex.lock();
    } else {
        // @synchronized(nil) 传入nil时什么也不处理
    }
    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        //根据obj指针的哈希值查找对应的SyncData,threadcount计数减一
        SyncData* data = id2data(obj, RELEASE);

        //使用SyncData的互斥锁解锁 
        bool okay = data->mutex.tryUnlock();
        if (!okay) {
           result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        }
    } else {
        // @synchronized(nil) 传入nil时什么也不处理
    }
    return result;
}

简单来说,调用objc_sync_enter(obj)时,会根据obj指针在哈希表sDataLists对应的链表SyncList,然后在链表中查询对应obj的SyncData对象,如果查询不到则创建一个新的SyncData对象(包含创建跟obj相关的递归互斥锁)并添加到链表中,然后使用SyncData对象上锁;调用objc_sync_exit(obj)时,使用SyncData对象解锁,因此通过这个锁便可确保@synchronized之间的代码线程安全。

sDataLists
深入dispatch_once

探讨了synchronized之后,我们再来说说dispatch_once。

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

根据官方文档,dispatch_once可以用来初始化一些全局的数据,它能够确保block代码在app的生命周期内仅被运行一次,而且还是线程安全的,不需要额外加锁;predicate必须指向一个全局或者静态的变量,不过使用predicate的话结果是未定义的,不过predicate有啥作用,如何实现block在整个生命周期执行一次?那我们只能从源码查找(源码地址:once)。
不过在这之前先简要介绍一下:

  • bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
    提供原子的比较和交换操作,如果当前值 *ptr == oldval,就将newval写入ptr,当比较赋值操作成功后返回true

  • *__sync_synchronize (...)
    调用这个函数会产生一个full memory barrier ,用于保证CPU按照我们代码编写的顺序来执行代码,比如:

doJob1();
 doJob2();
 __sync_synchronize();  //Job3会在Job1跟Job2完成后才执行
doJob3();
  • type __sync_swap(type *ptr, type value, ...)
    提供原子交换操作的函数,交换第一个跟第二个参数的值,然后返回交换前第一个参数的旧值。
  • _dispatch_hardware_pause()
    调用这个函数主要是暗示处理器不要做额外的优化处理等,提高性能,节省CPU时间,可以查看这里了解更多
  • 信号量
    信号量是一个非负整数,定义了两种原子操作:wait跟signal来进行访,信号量主要用于线程同步。当一个线程调用wait操作时,如果信号量的值大于0,则获得资源并将信号量值减一,如果等于0线程睡眠直到信号量值大于0或者超时;singal将信号量的值加1,如果这时候有正在等待的线程,唤醒该线程。
// 创建一个信号量,其值为0        
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
    //操作完成后,调用signal信号量+1
    dispatch_semaphore_signal(sema);
});
//等待dispatch_semaphore_signal将信号量值加1后才继续运行
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

接下来看看具体代码,当我们调用dispatch_once时候,内部是调用dispatch_once_f函数,其中val就是外部传入的predicate值,ctxt为Block的指针,func则是Block内部具体实现的函数指针,由于源码比较短,所以我直接把源码贴出来(为了方便查看,有些不使用宏定义)。

struct _dispatch_once_waiter_s {
    volatile struct _dispatch_once_waiter_s *volatile dow_next;
    _dispatch_thread_semaphore_t dow_sema;
};
#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)
void dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
    struct Block_basic *bb = (void *)block;
    dispatch_once_f(val, block, (void *)bb->Block_invoke);
}

void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    //volatile,标示该变量随时可能改变,编译器不会对访问该变量的代码进行优化,每次都从内存去读取,而不使用寄存器里的值
    struct _dispatch_once_waiter_s * volatile *vval =
            (struct _dispatch_once_waiter_s**)val;
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;

    //第一次执行的时候,predicate的值为0,所以vval=NULL,原子比较交换函数返回true
    //然后vval指向dow(dispatch_once_waiter_s,信号量的值为0,即等待中)
    if (__sync_bool_compare_and_swap(vval, NULL, &dow)) {

        //空的宏定义,啥也不做
        dispatch_atomic_acquire_barrier();

        //执行dispatch_once传进来的block
        _dispatch_client_callout(ctxt, func);
        
        //后面解释
        dispatch_atomic_maximally_synchronizing_barrier();
        
        //执行完block之后,将vval的值设为DISPATCH_ONCE_DONE(即predicate设为~0l)
        tmp = __sync_swap(vval, DISPATCH_ONCE_DONE);  
        tail = &dow;

        //1.如果在block的执行过程中,没有其线程调用该函数等待,tmp的值也为&dow,tail==tmp,循环的条件不满足,函数执行完毕
        //2.如果在block的执行过程中,有其线程调用该函数等待,历遍信号量链表,逐个唤醒线程继续运行
        while (tail != tmp) {
            //如果中途有其它线程将vval赋值&dow,这期间dow_next值为NULL,需要等待,参见else分支的__sync_bool_compare_and_swap调用
            while (!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    }
    else 
    {   
        //如果vval不等NULL,走这个分支,非第一次调用dispatch_once,其它线程调用
        //获取信号量,如果有信号量则返回该信号量,如果没有则在当前线程创建一个新的信号量
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for (;;) {
            tmp = *vval;

            //vval已经被赋值为~0l,证明block已经被执行了,退出然后调用_dispatch_put_thread_semaphore销毁信号量
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            //空的宏定义,啥也不做
            dispatch_atomic_store_barrier();

            //将当前信号量加入到信号链表中,然后线程等待,
            if (__sync_bool_compare_and_swap(vval, tmp, &dow)) {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }

            //如果vval的指向值不再是tmp,可能其它线程同时进入该分支,然后调用__sync_bool_compare_and_swap原子操作将vval指向了新的节点,
            //则重新开始for循环
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}

让我们来看看dispatch_once是如何确保block只执行一次。简单来说,当线程A在调用执行block并设置predicate为DISPATCH_ONCE_DONE(~0l)期间,如果有其他线程也在调用disptach_once,则这些线程会等待,各线程对应的信号量会加入到信号量链表中,等predicate设置为DISPATCH_ONCE_DONE后,也就是block执行完了,会根据信号量链表唤醒各个线程使其继续执行。


信号量链表.png

  不过有一种临界情况,假如线程A在执行block,但是创建单例对象obj还未完成,这时候线程B获取该obj对象,此时obj=nil,而线程B在线程A将predicate设为DISPATCH_ONCE_DONE之后读取predicate,这是线程B会认为单例对象已经初始化完成,然后使用空的obj对象,这就会导致错误发生。因此dispatch_once会在执行完block之后会执行dispatch_atomic_maximally_synchronizing_barrier()调用,这个调用会执行一些cpuid指令,确保线程A创建单例对象obj以及置predicate为DISPATCH_ONCE_DONE的时间TimeA大于线程B进入block并读取predicate值的时间TimeB。

#define dispatch_atomic_maximally_synchronizing_barrier() \
    do { unsigned long _clbr; __asm__ __volatile__( \
    "cpuid" \
    : "=a" (_clbr) : "0" (0) : "ebx", "ecx", "edx", "cc", "memory" \
    ); } while(0)

除此之外,每次调用dispatch_once的时候,都会先判断predicate的值是否是~0l(也就是DISPATCH_ONCE_DONE),如果是则意味着block已经执行过了,便不再执行,代码如下:

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
#ifdef __GNUC__
#define dispatch_once(x, ...) do { if (__builtin_expect(*(x), ~0l) != ~0l) dispatch_once((x), (__VA_ARGS__)); } while (0)
#endif

让我们看看这里面的__builtin_expect((x), (v)),这又是一个优化的地方。。。

__builtin_expect()目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,
以减少指令跳转带来的性能下降。
__builtin_expect((x),1) 表示 x 的值为真的可能性更大; 
__builtin_expect((x),0) 表示 x 的值为假的可能性更大。  

由于dispatch_once的只执行block一次,所以我们更期望的是已经block已经执行完了,也就是predict的值为~0l的可能性更大。
  现在我们清楚dispatch_once是如何确保block只执行一次了,关键就在predict这个值,通过比较这个值等于0或者~0l来判断block是否执行过,这也就是为啥我们需要将这个值设为static或者全局的缘故,因为各个线程都要去访问这个predict,有兴趣的可以试试把predicate的初始值设为非0或者非静态全局变量会发生什么~~

总结

通过上面的分析,我们知道@synchronized采用的是递归互斥锁来实现线程安全,而dispatch_once的内部则使用了很多原子操作来替代锁,以及通过信号量来实现线程同步,而且有很多针对处理器优化的地方,甚至在if判断语句上也做了优化(逼格有点高),使得其效率有很大的提升,虽然其源码很短,但里面包含的东西却很多,所以苹果也推荐使用dispatch_once来创建单例。通过这个简短的dispatch_once,你也可以清楚为什么GCD的性能会这么高了,感兴趣可以再去看看libdispatch的其它源码。。

参考

objc-sync
synchronized
dispatch_once
Built-in functions for atomic memory access
__builtin_expect

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

推荐阅读更多精彩内容