細說@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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章