ios @synchronized的實現(xiàn)原理

@synchronized 本質(zhì)是個遞歸鎖伍玖,不需要程序員手動加解鎖窍箍,并且不會產(chǎn)生死鎖問題丽旅,因此在開發(fā)中的使用頻率比較高纺棺,下面我們來研究一下他的底層實現(xiàn)祷蝌。

一、底層調(diào)用實現(xiàn)

@synchronized是個關鍵字米丘,沒法在代碼中直接跳轉(zhuǎn)查看定義罪郊,最直接的辦法就是打上斷點尚洽、看匯編:

斷點.png

代碼跑起來看匯編:
匯編.png

在匯編中腺毫,有兩個關鍵代碼潮酒,objc_sync_enterobjc_sync_exit扎狱,他們對應的就是加鎖解鎖操作勃教。

當然我們也可以通過clang(可以參考:ios 編譯調(diào)試技巧
)來看一下故源,整理后@synchronized對應的代碼如下:

{
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            
            objc_sync_enter(_sync_obj);
            try {
                struct _SYNC_EXIT {
                    _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
                    id sync_exit;
                } _sync_exit(_sync_obj);
                
                NSLog(@"-----");
            } catch (id e) {
                _rethrow = e;
                
            }
            
            {
                struct _FIN {
                    _FIN(id reth) : rethrow(reth) {}
                    ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                    id rethrow;
                } _fin_force_rethow(_rethrow);
            }
        }

二、源碼實現(xiàn)

2.1 objc_sync_enter

我們這里用的是objc4-756.2的源碼绳军,搜索objc_sync_enter找到實現(xiàn):

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

函數(shù)注釋:

  • 開始在obj上進行同步
  • 如果需要门驾,分配與obj關聯(lián)的遞歸互斥體。
  • 獲取鎖之后楣责,返回OBJC_SYNC_SUCCESS腐魂。從代碼看,返回始終未成功削樊,這是因為當鎖被占用時兔毒,會阻塞 (data->mutex.lock();)到鎖釋放,然后往下執(zhí)行迅脐。

如果obj則不會加解鎖豪嗽,但是被包裹在@synchronized(nil){}中的代碼塊依然會正常執(zhí)行龟梦,因為沒有阻塞當前線程。也就是說:加鎖失敗钦睡,不影響代碼繼續(xù)向下執(zhí)行躁倒。

2.2 id2data()函數(shù)實現(xiàn)

SyncData* data = id2data(obj, ACQUIRE);
data->mutex.lock();

先來了解一下SyncData

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

這個結(jié)構(gòu)體有四個對象秧秉,:
nextData:指向下一個SyncData,這看上去是一個單向鏈表撩嚼。
object:對象指針挖帘,objc_objectOC對象拇舀,它保存了被鎖定對象obj(@synchronized(obj))的指針
threadCount:記錄正在使用這個代碼塊的線程數(shù)
mutex:遞歸鎖,獲取到SyncData對象后聘鳞,即調(diào)用它的lock()方法

下面來看id2data()的實現(xiàn),這段代碼很長站楚,涉及到查找緩存搏嗡、對象鎖鏈表節(jié)點的創(chuàng)建插入采盒,我們分段分析:

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    // ........
    return result;
}

lockp:從命名看似乎是自旋鎖,但是看關系鏈:spinlock_t -> mutex_tt<LOCKDEBUG> -> os_unfair_lock 尺栖,os_unfair_lock就是用來替代OSSpinLock這個自旋鎖的互斥鎖烦租,文檔注釋:

@discussion
已淘汰的OSSpinLock的替代品左权。 不會發(fā)生爭搶,只會等待內(nèi)核被解鎖喚醒。
與OSSpinLock一樣獲取鎖是無序的蠢棱,例如未鎖定者在有機會嘗試獲取鎖之前泻仙,解鎖者可能會立即重新獲取鎖。這對于性能可能是有利的突想,但是也可能使等待者挨餓究抓。

listp:SyncData的二重指針刺下,剛才知道SyncData是個鏈表,這個listp就是鏈表的頭指針
result:鎖定對象obj關聯(lián)的SyncData結(jié)構(gòu)體工腋。

2.2.1 快速緩存

檢查當前線程單項快速緩存中是否有匹配的對象

// Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

fastCacheOccupied:標記線程的快速緩存是否已占用擅腰,如果找到就標記為YES趁冈,無論這個緩存是否關聯(lián)了當前要被鎖定的對象
data:當前線程的私有數(shù)據(jù),非所有數(shù)據(jù)矾飞,只是鏈表中某個節(jié)點的指針
如果快速緩存中恰好是和當前對象關聯(lián)的鎖呀邢,那么對這個鎖計數(shù)+1,如果是解鎖申眼,就-1括尸。
快速緩存中的SyncData和鎖的計數(shù)病毡,都屬于線程的私有數(shù)據(jù),是當前線程獨有的有送,其他線程訪問不到僧家。

2.2.2 線程整體緩存

線程私有數(shù)據(jù)只保存一個節(jié)點的地址八拱,如果沒有,還要從線程的整體緩存中查找清蚀,檢查已擁有鎖的線程整體緩存中是否有匹配的對象:

// Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

SyncCache的結(jié)構(gòu):

typedef struct SyncCache {
    //可以保存SyncCacheItem的總數(shù)轧铁,已開辟的緩存空間齿风,默認為4,2倍擴容
    unsigned int allocated;  
    unsigned int used;        //保存已使用數(shù)量
    SyncCacheItem list[0]; //以及緩存鏈表的頭節(jié)點地址
} SyncCache;

SyncCacheItem的結(jié)構(gòu):

typedef struct {
    SyncData *data; 
    unsigned int lockCount;  // number of times THIS THREAD locked this block 
} SyncCacheItem;

lockCount:當前線程持鎖計數(shù)器
如果緩存命中童本,加鎖則對持鎖計數(shù)器+1脸候,如果是釋放鎖就-1运沦;并且當持鎖計數(shù)器為0的時候,要將已使用數(shù)SyncCache->used做-1操作嫁盲。

2.2.3 無緩存

如果沒有緩存烈掠,會操作使用清單listp,使用空閑節(jié)點或創(chuàng)建新節(jié)點:

// Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            //開始遍歷鏈表,如果找到節(jié)點中存在當前對象的鎖矫限,goto done
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //記錄第一個空閑節(jié)點
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
        
        //鏈表中有空閑節(jié)點奇唤,直接征用這個節(jié)點把它和當前對象關聯(lián)起來
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

     //鏈表中節(jié)點都已被使用咬扇,且沒有和當前對象相關聯(lián)的節(jié)點廊勃,那么創(chuàng)建一個新節(jié)點,并插入到鏈表的頭部
    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;

firstUnused:局部變量梭灿,用于記錄鏈表中第一個沒有使用的節(jié)點堡妒。由于程序中可能會對很多對象使用鎖溉卓,但是使用完了之后這個節(jié)點還在鏈表中而占用它的線程已經(jīng)沒有了,那么這個節(jié)點就可以被拿來直接用于當前的對象伏尼,省的再去開辟新的內(nèi)存空間插入鏈表爆阶,即省時又剩空間班套。

未找到緩存故河,同時有了新的SyncData節(jié)點忧勿,那么更新線程緩存:

done:
    lockp->unlock();
    if (result) {
    
#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

fastCacheOccupied:1、為NO熏挎,線程快速緩存沒有被占用晌砾,則將結(jié)果保存到快速緩存;2哼勇、為YES呕乎,線程快速緩存已被占用,將節(jié)點保存到線程整體緩存中

從前面的代碼知道帝璧,只要線程快速緩存存在的烁,無論是否命中當前需要被鎖的對象诈闺,fastCacheOccupied都會被置為YES。也就是說襟雷,線程私有數(shù)據(jù)的快速緩存只緩存第一次,且只保存第一次的這一個節(jié)點指針注盈。
蘋果為什么這么做叙赚,個人猜想:
1震叮、線程第一次使用同步鎖鎖定的對象,也很可能是該線程需要鎖定頻率最高的對象尉间,比如我們經(jīng)常使用@synchronized(self)
2击罪、該對象的同步鎖可能已經(jīng)被其他線程緩存到私有數(shù)據(jù)了媳禁,當前線程又無法訪問其他線程的私有數(shù)據(jù),如果替換的話囱怕,會重復緩存

2.2.4 listp

無緩存時從鏈表取對象毫别,那么保存對象鎖的鏈表具體是什么樣子的呢:

spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
//使用多個并行列表來減少不相關對象之間的爭用岛宦。
// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

關于SyncList

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
}

從全局靜態(tài)變量sDataLists砾肺,以obj為索引獲取到的對象類型為StripedMap<SyncList>,同時對其取地址&SyncList.data后返回。下面是StripedMap部分源碼:

// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // 哈希函數(shù)
    }

public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }

//.......

}

T& operator[]:C++操作符重載疫衩,通過obj取值T<SyncList>
indexForPointer:數(shù)組的索引是通過一個hash算法獲取
雖然真機的hash表大小只有8闷煤,但是這個方法取到得的是鏈表,雖然不同的對象可能取到同一個鏈表假褪,但鏈表中有多個節(jié)點近顷,每個節(jié)點又保存了和不同對象相關聯(lián)的鎖窒升,這樣就避免了hash沖突

sDataLists.png

饱须。

2.3 解鎖 objc_sync_exit()

主要通過函數(shù)id2data()獲得鎖,然后tryUnlock()譬挚。

// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    

    return result;
}

三减宣、總結(jié)

  1. @synchronized()是遞歸鎖号杠,同一線程可重入姨蟋,只是內(nèi)部有個持鎖計數(shù)器而已
  2. 進入@synchronized()代碼塊時會執(zhí)行objc_sync_enter(id obj)加鎖
  3. 核心方法是通過id2data()來獲取到對象鎖節(jié)點SyncData
    3.1. 首先從當前線程的私有數(shù)據(jù)(快速緩存)中查找
    3.2. 從當前線程整體緩存中查找,檢查已擁有鎖的線程緩存中是否有匹配的對象
    3.3. 從全局靜態(tài)listp對象鎖鏈表中查找悠砚,并更新線程緩存
  4. 退出@synchronized()代碼塊灌旧,執(zhí)行objc_sync_enter(id obj)解鎖

lockCount:被鎖次數(shù)绰筛,可遞歸重入
threadCount:SyncData影響的多線程統(tǒng)計

注意點:
在使用@synchronized(obj){}時,如果obj為nil衡蚂,就不會加鎖毛甲,而代碼塊中的代碼依然會正常執(zhí)行,那就會存在風險只损,如下:

for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
        });
    }

多線程中_testArray可能會被release置nil七咧,這個時候會加鎖失敗坑雅,同時如果發(fā)生多次release,就會crash终蒂。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拇泣,一起剝皮案震驚了整個濱河市矮锈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌债朵,老刑警劉巖序芦,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谚中,死亡現(xiàn)場離奇詭異寥枝,居然都是意外死亡囊拜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門来吩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敢辩,“玉大人,你說我怎么就攤上這事弟疆。” “怎么了盗冷?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵怠苔,是天一觀的道長。 經(jīng)常有香客問我仪糖,道長柑司,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任锅劝,我火速辦了婚禮,結(jié)果婚禮上故爵,老公的妹妹穿的比我還像新娘玻粪。我一直安慰自己,他們只是感情好诬垂,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布劲室。 她就那樣靜靜地躺著,像睡著了一般结窘。 火紅的嫁衣襯著肌膚如雪很洋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天隧枫,我揣著相機與錄音喉磁,去河邊找鬼。 笑死官脓,一個胖子當著我的面吹牛协怒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播确买,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼斤讥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了湾趾?” 一聲冷哼從身側(cè)響起芭商,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搀缠,沒想到半個月后铛楣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡艺普,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年簸州,在試婚紗的時候發(fā)現(xiàn)自己被綠了鉴竭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡岸浑,死狀恐怖搏存,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情矢洲,我是刑警寧澤璧眠,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站读虏,受9級特大地震影響责静,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盖桥,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一灾螃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揩徊,春花似錦腰鬼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至袜炕,卻和暖如春本谜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偎窘。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工乌助, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陌知。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓他托,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仆葡。 傳聞我的和親對象是個殘疾皇子赏参,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359