iOS-底層原理 29:鎖的原理

iOS 底層原理 文章匯總

本文主要介紹常見的鎖捡絮,以及synchronized牙躺、NSLock、遞歸鎖均驶、條件鎖的底層分析

借鑒一張鎖的性能數(shù)據(jù)對比圖颓影,如下所示

鎖性能對比

可以看出,圖中鎖的性能從高到底依次是:OSSpinLock(自旋鎖) -> dispatch_semaphone(信號量) -> pthread_mutex(互斥鎖) -> NSLock(互斥鎖) -> NSCondition(條件鎖) -> pthread_mutex(recursive 互斥遞歸鎖) -> NSRecursiveLock(遞歸鎖) -> NSConditionLock(條件鎖) -> synchronized(互斥鎖)

圖中鎖大致分為以下幾類:

  • 【1盾鳞、自旋鎖】:在自旋鎖中潘酗,線程會(huì)反復(fù)檢查變量是否可用。由于線程這個(gè)過程中一致保持執(zhí)行雁仲,所以是一種忙等待仔夺。 一旦獲取了自旋鎖,線程就會(huì)一直保持該鎖攒砖,直到顯式釋放自旋鎖缸兔。自旋鎖避免了進(jìn)程上下文的調(diào)度開銷,因此對于線程只會(huì)阻塞很短時(shí)間的場合有效的吹艇。對于iOS屬性的修飾符atomic惰蜜,自帶一把自旋鎖

    • OSSpinLock

    • atomic

  • 【2、互斥鎖】:互斥鎖是一種用于多線程編程中受神,防止兩條線程同時(shí)對同一公共資源(例如全局變量)進(jìn)行讀寫的機(jī)制抛猖,該目的是通過將代碼切成一個(gè)個(gè)臨界區(qū)而達(dá)成

    • @synchronized

    • NSLock

    • pthread_mutex

  • 【3、條件鎖】:條件鎖就是條件變量鼻听,當(dāng)進(jìn)程的某些資源要求不滿足時(shí)就進(jìn)入休眠财著,即鎖住了,當(dāng)資源被分配到了撑碴,條件鎖打開了撑教,進(jìn)程繼續(xù)運(yùn)行

    • NSCondition

    • NSConditionLock

  • 【4、遞歸鎖】:遞歸鎖就是同一個(gè)線程可以加鎖N次而不會(huì)引發(fā)死鎖醉拓。遞歸鎖是特殊的互斥鎖伟姐,即是帶有遞歸性質(zhì)的互斥鎖

    • pthread_mutex(recursive)

    • NSRecursiveLock

  • 【5、信號量】:信號量是一種更高級的同步機(jī)制亿卤,互斥鎖可以說是semaphore在僅取值0/1時(shí)的特例愤兵,信號量可以有更多的取值空間,用來實(shí)現(xiàn)更加復(fù)雜的同步排吴,而不單單是線程間互斥

    • dispatch_semaphore
  • 【6秆乳、讀寫鎖】:讀寫鎖實(shí)際是一種特殊的自旋鎖。將對共享資源的訪問分成讀者寫者傍念,讀者只對共享資源進(jìn)行讀訪問矫夷,寫者則需要對共享資源進(jìn)行寫操作。這種鎖相對于自旋鎖而言憋槐,能提高并發(fā)性

    • 一個(gè)讀寫鎖同時(shí)只能有一個(gè)寫者或者多個(gè)讀者双藕,但不能既有讀者又有寫者,在讀寫鎖保持期間也是搶占失效的

    • 如果讀寫鎖當(dāng)前沒有讀者阳仔,也沒有寫者忧陪,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里近范, 直到?jīng)]有任何寫者或讀者嘶摊。如果讀寫鎖沒有寫者,那么讀者可以立

其實(shí)基本的鎖就包括三類:自旋鎖评矩、互斥鎖叶堆、讀寫鎖,其他的比如條件鎖斥杜、遞歸鎖虱颗、信號量都是上層的封裝和實(shí)現(xiàn)

1蔗喂、OSSpinLock(自旋鎖)

自從OSSpinLock出現(xiàn)安全問題忘渔,在iOS10之后就被廢棄了。自旋鎖之所以不安全缰儿,是因?yàn)?code>獲取鎖后畦粮,線程會(huì)一直處于忙等待,造成了任務(wù)的優(yōu)先級反轉(zhuǎn)乖阵。

其中的忙等待機(jī)制可能會(huì)造成高優(yōu)先級任務(wù)一直running等待宣赔,占用時(shí)間片,而低優(yōu)先級的任務(wù)無法搶占時(shí)間片瞪浸,會(huì)造成一直不能完成拉背,鎖未釋放的情況

OSSpinLock被棄用后,其替代方案是內(nèi)部封裝了os_unfair_lock默终,而os_unfair_lock在加鎖時(shí)會(huì)處于休眠狀態(tài)椅棺,而不是自旋鎖的忙等狀態(tài)

2、atomic(原子鎖)

atomic適用于OC中屬性的修飾符齐蔽,其自帶一把自旋鎖两疚,但是這個(gè)一般基本不使用,都是使用的nonatomic

在前面的文章中含滴,我們提及setter方法會(huì)根據(jù)修飾符調(diào)用不同方法诱渤,其中最后會(huì)統(tǒng)一調(diào)用reallySetProperty方法,其中就有atomic非atomic的操作

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
   ...
   id *slot = (id*) ((char*)self + offset);
   ...

    if (!atomic) {//未加鎖
        oldValue = *slot;
        *slot = newValue;
    } else {//加鎖
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    ...
}

從源碼中可以看出谈况,對于atomic修飾的屬性勺美,進(jìn)行了spinlock_t加鎖處理递胧,但是在前文中提到OSSpinLock已經(jīng)廢棄了,這里的spinlock_t在底層是通過os_unfair_lock替代了OSSpinLock實(shí)現(xiàn)的加鎖赡茸。同時(shí)為了防止哈希沖突缎脾,還是用了加鹽操作

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    ...
}

getter方法中對atomic的處理,同setter是大致相同的

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();//加鎖
    id value = objc_retain(*slot);
    slotlock.unlock();//解鎖
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

3占卧、synchronized(互斥遞歸鎖)

  • 開啟匯編調(diào)試遗菠,發(fā)現(xiàn)@synchronized在執(zhí)行過程中,會(huì)走底層的objc_sync_enterobjc_sync_exit方法

    synchronized-1

  • 也可以通過clang华蜒,查看底層編譯代碼

    synchronized-2

  • 通過對objc_sync_enter方法符號斷點(diǎn)辙纬,查看底層所在的源碼庫,通過斷點(diǎn)發(fā)現(xiàn)在objc源碼中叭喜,即libobjc.A.dylib

    synchronized-3

objc_sync_enter & objc_sync_exit 分析

  • 進(jìn)入objc_sync_enter源碼實(shí)現(xiàn)
    • 如果obj存在贺拣,則通過id2data方法獲取相應(yīng)的SyncData,對threadCount捂蕴、lockCount進(jìn)行遞增操作
    • 如果obj不存在纵柿,則調(diào)用objc_sync_nil,通過符號斷點(diǎn)得知启绰,這個(gè)方法里面什么都沒做昂儒,直接return了
      objc_sync_nil
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {//傳入不為nil
        SyncData* data = id2data(obj, ACQUIRE);//重點(diǎn)
        ASSERT(data);
        data->mutex.lock();//加鎖
    } else {//傳入nil
        // @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;
}
  • 進(jìn)入objc_sync_exit源碼實(shí)現(xiàn)
    • 如果obj存在,則調(diào)用id2data方法獲取對應(yīng)的SyncData委可,對threadCount渊跋、lockCount進(jìn)行遞減操作
    • 如果obj為nil,什么也不做
// End synchronizing on 'obj'. 結(jié)束對“ 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) {//obj不為nil
        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 {//obj為nil時(shí)着倾,什么也不做
        // @synchronized(nil) does nothing
    }
    return result;
}

通過上面兩個(gè)實(shí)現(xiàn)邏輯的對比拾酝,發(fā)現(xiàn)它們有一個(gè)共同點(diǎn),在obj存在時(shí)卡者,都會(huì)通過id2data方法蒿囤,獲取SyncData

  • 進(jìn)入SyncData的定義,是一個(gè)結(jié)構(gòu)體崇决,主要用來表示一個(gè)線程data材诽,類似于鏈表結(jié)構(gòu),有next指向恒傻,且封裝了recursive_mutex_t屬性脸侥,可以確認(rèn)@synchronized確實(shí)是一個(gè)遞歸互斥鎖
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//類似鏈表結(jié)構(gòu)
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;//遞歸鎖
} SyncData;
  • 進(jìn)入SyncCache的定義,也是一個(gè)結(jié)構(gòu)體盈厘,用于存儲(chǔ)線程睁枕,其中list[0]表示當(dāng)前線程的鏈表data,主要用于存儲(chǔ)SyncDatalockCount
typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

id2data 分析

  • 進(jìn)入id2data源碼,從上面的分析外遇,可以看出注簿,這個(gè)方法是加鎖和解鎖都復(fù)用的方法
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的線程緩存)
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //通過KVC方式對線程進(jìn)行獲取 線程綁定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    //如果線程緩存中有data跳仿,執(zhí)行if流程
    if (data) {
        fastCacheOccupied = YES;
        //如果在線程空間找到了data
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            //通過KVC獲取lockCount诡渴,lockCount用來記錄 被鎖了幾次,即 該鎖可嵌套
            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: {
                //objc_sync_enter走這里塔嬉,傳入的是ACQUIRE -- 獲取
                lockCount++;//通過lockCount判斷被鎖了幾次,即表示 可重入(遞歸鎖如果可重入租悄,會(huì)死鎖)
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//設(shè)置
                break;
            }
            case RELEASE:
                //objc_sync_exit走這里谨究,傳入的why是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;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);//判斷緩存中是否有該線程
    //如果cache中有,方式與線程緩存一致
    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中清除使用標(biāo)記
                    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;
        }
    }

    // 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.
    //第一次進(jìn)來泣棋,所有緩存都找不到
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {//cache中已經(jīng)找到
            if ( p->object == object ) {//如果不等于空胶哲,且與object相似
                result = p;//賦值
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);//對threadCount進(jìn)行++
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object 沒有與當(dāng)前對象關(guān)聯(lián)的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 第一次進(jìn)來,沒有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // 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));//創(chuàng)建賦值
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) { //判斷是否支持棧存緩存潭辈,支持則通過KVC形式賦值 存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
        } else 
#endif
        {
            // Save in thread cache 緩存中存一份
            if (!cache) cache = fetch_cache(YES);//第一次存儲(chǔ)時(shí)鸯屿,對線程進(jìn)行了綁定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}
  • 【第一步】首先在tls線程緩存中查找。
    • tls_get_direct方法中以線程為key把敢,通過KVC的方式獲取與之綁定的SyncData寄摆,即線程data。其中的tls()修赞,表示本地局部的線程緩存婶恼,

    • 判斷獲取的data是否存在,以及判斷data中是否能找到對應(yīng)的object

    • 如果都找到了柏副,在tls_get_direct方法中以KVC的方式獲取lockCount勾邦,用來記錄對象被鎖了幾次(即鎖的嵌套次數(shù))

    • 如果data中的threadCount 小于等于0,或者 lockCount 小于等于0時(shí)割择,則直接崩潰

    • 通過傳入的why眷篇,判斷是操作類型

      • 如果是ACQUIRE,表示加鎖荔泳,則進(jìn)行lockCount++蕉饼,并保存到tls緩存

      • 如果是RELEASE,表示釋放玛歌,則進(jìn)行lockCount--椎椰,并保存到tls緩存。如果lockCount 等于 0沾鳄,從tls中移除線程data

      • 如果是CHECK慨飘,則什么也不做

  • 【第二步】如果tls中沒有,則在cache緩存中查找

    • 通過fetch_cache方法查找cache緩存中是否有線程

    • 如果有,則遍歷cache總表瓤的,讀取出線程對應(yīng)的SyncCacheItem

    • SyncCacheItem中取出data休弃,然后后續(xù)步驟與tls的匹配是一致的

  • 【第三步】如果cache中也沒有,即第一次進(jìn)來圈膏,則創(chuàng)建SyncData塔猾,并存儲(chǔ)到相應(yīng)緩存中

    • 如果在cache中找到線程,且與object相等稽坤,則進(jìn)行賦值丈甸、以及threadCount++
    • 如果在cache中沒有找到,則threadCount等于1

所以在id2data方法中尿褪,主要分為三種情況

  • 【第一次進(jìn)來睦擂,沒有鎖】:
    • threadCount = 1

    • lockCount = 1

    • 存儲(chǔ)到tls

  • 【不是第一次進(jìn)來,且是同一個(gè)線程】
    • tls中有數(shù)據(jù)杖玲,則lockCount++

    • 存儲(chǔ)到tls

  • 【不是第一次進(jìn)來顿仇,且是不同線程】
    • 全局線程空間進(jìn)行查找線程

    • threadCount++

    • lockCount++

    • 存儲(chǔ)到cache

tls和cache表結(jié)構(gòu)
針對tls和cache緩存,底層的表結(jié)構(gòu)如下所示

tls和cache緩存結(jié)構(gòu)

  • 哈希表結(jié)構(gòu)中通過SyncList結(jié)構(gòu)來組裝多線程的情況

  • SyncData通過鏈表的形式組裝當(dāng)前可重入的情況

  • 下層通過tls線程緩存摆马、cache緩存來進(jìn)行處理

  • 底層主要有兩個(gè)東西:lockCount臼闻、threadCount,解決了遞歸互斥鎖,解決了嵌套可重入

@synchronized 坑點(diǎn)

下面代碼這樣寫囤采,會(huì)有什么問題述呐?

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

運(yùn)行結(jié)果發(fā)現(xiàn),運(yùn)行就崩潰

synchronized 坑點(diǎn)-1

崩潰的主要原因是testArray在某一瞬間變成了nil蕉毯,從@synchronized底層流程知道市埋,如果加鎖的對象成了nil,是鎖不住的恕刘,相當(dāng)于下面這種情況缤谎,block內(nèi)部不停的retain、release褐着,會(huì)在某一瞬間上一個(gè)還未release螃宙,下一個(gè)已經(jīng)準(zhǔn)備release缅阳,這樣會(huì)導(dǎo)致野指針的產(chǎn)生

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

可以根據(jù)上面的代碼演熟,打開edit scheme -> run -> Diagnostics中勾選Zombie Objects 帮哈,來查看是否是僵尸對象,結(jié)果如下所示

synchronized 坑點(diǎn)-2

我們一般使用@synchronized (self)馅扣,主要是因?yàn)?code>_testArray的持有者是self

注意:野指針 vs 過渡釋放

  • 野指針:是指由于過渡釋放產(chǎn)生的指針還在進(jìn)行操作

  • 過渡釋放:每次都會(huì)retain 和 release

總結(jié)

  • @synchronized在底層封裝的是一把遞歸鎖斟赚,所以這個(gè)鎖是遞歸互斥鎖

  • @synchronized的可重入,即可嵌套差油,主要是由于lockCountthreadCount的搭配

  • @synchronized使用鏈表的原因是鏈表方便下一個(gè)data的插入拗军,

  • 但是由于底層中鏈表查詢任洞、緩存的查找以及遞歸,是非常耗內(nèi)存以及性能的发侵,導(dǎo)致性能低交掏,所以在前文中,該鎖的排名在最后

  • 但是目前該鎖的使用頻率仍然很高刃鳄,主要是因?yàn)?code>方便簡單盅弛,且不用解鎖

  • 不能使用非OC對象作為加鎖對象,因?yàn)槠?code>object的參數(shù)為id

  • @synchronized (self)這種適用于嵌套次數(shù)較少的場景叔锐。這里鎖住的對象也并不永遠(yuǎn)是self挪鹏,這里需要讀者注意

  • 如果鎖嵌套次數(shù)較多,即鎖self過多愉烙,會(huì)導(dǎo)致底層的查找非常麻煩讨盒,因?yàn)槠涞讓邮擎湵磉M(jìn)行查找,所以會(huì)相對比較麻煩齿梁,所以此時(shí)可以使用NSLock催植、信號量

4肮蛹、NSLock

NSLock是對下層pthread_mutex的封裝勺择,使用如下

 NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];

直接進(jìn)入NSLock定義查看,其遵循了NSLocking協(xié)議伦忠,下面來探索NSLock的底層實(shí)現(xiàn)

NSLock 底層分析

  • 通過加符號斷點(diǎn)lock分析省核,發(fā)現(xiàn)其源碼在Foundation框架中,

    NSLock分析-1

  • 由于OC的Foundation框架不開源昆码,所以這里借助Swift的開源框架Foundation來 分析NSLock的底層實(shí)現(xiàn)气忠,其原理與OC是大致相同的

    NSLock分析-2

    通過源碼實(shí)現(xiàn)可以看出,底層是通過pthread_mutex互斥鎖實(shí)現(xiàn)的赋咽。并且在init方法中旧噪,還做了一些其他操作,所以在使用NSLock時(shí)需要使用init初始化

回到前文的性能圖中脓匿,可以看出NSLock的性能僅次于 pthread_mutex(互斥鎖)淘钟,非常接近

使用弊端

請問下面block嵌套block的代碼中,會(huì)有什么問題陪毡?

for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
        };
        testMethod(10);
    });
}  
  • 在未加鎖之前米母,其中的current=9、10有很多條毡琉,導(dǎo)致數(shù)據(jù)混亂铁瞒,主要原因是多線程導(dǎo)致的


    NSLock分析-3
  • 如果像下面這樣加鎖,會(huì)有什么問題桅滋?

NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
        };
        testMethod(10);
        [lock unlock];
    });
}  

其運(yùn)行結(jié)果如下

NSLock分析-4

會(huì)出現(xiàn)一直等待的情況慧耍,主要是因?yàn)?code>嵌套使用的遞歸,使用NSLock(簡單的互斥鎖,如果沒有回來蜂绎,會(huì)一直睡覺等待)栅表,即會(huì)存在一直加lock,等不到unlock 的堵塞情況

所以师枣,針對這種情況怪瓶,可以使用以下方式解決

  • 使用@synchronized
for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            @synchronized (self) {
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
            }
        };
        testMethod(10); 
    });
}
  • 使用遞歸鎖NSRecursiveLock
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
 for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        [recursiveLock lock];
        testMethod = ^(int value){
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [recursiveLock unlock];
        };
        testMethod(10);
    });
}

5、pthread_mutex

pthread_mutex就是互斥鎖本身践美,當(dāng)鎖被占用洗贰,其他線程申請鎖時(shí),不會(huì)一直忙等待陨倡,而是阻塞線程并睡眠

使用

// 導(dǎo)入頭文件
#import <pthread.h>

// 全局聲明互斥鎖
pthread_mutex_t _lock;

// 初始化互斥鎖
pthread_mutex_init(&_lock, NULL);

// 加鎖
pthread_mutex_lock(&_lock);
// 這里做需要線程安全操作
// 解鎖 
pthread_mutex_unlock(&_lock);

// 釋放鎖
pthread_mutex_destroy(&_lock);

6敛滋、NSRecursiveLock

NSRecursiveLock在底層也是對pthread_mutex的封裝,可以通過swiftFoundation源碼查看

NSRecursiveLock-1

對比NSLockNSRecursiveLock兴革,其底層實(shí)現(xiàn)幾乎一模一樣绎晃,區(qū)別在于init時(shí),NSRecursiveLock有一個(gè)標(biāo)識(shí)PTHREAD_MUTEX_RECURSIVE杂曲,而NSLock是默認(rèn)的

NSRecursiveLock-2

遞歸鎖主要是用于解決一種嵌套形式庶艾,其中循環(huán)嵌套居多

7、NSCondition

NSCondition 是一個(gè)條件鎖擎勘,在日常開發(fā)中使用較少咱揍,與信號量有點(diǎn)相似:線程1需要滿足條件1才會(huì)往下走,否則會(huì)堵塞等待棚饵,知道條件滿足煤裙。經(jīng)典模型是生產(chǎn)消費(fèi)者模型

NSCondition的對象實(shí)際上作為一個(gè) 和 一個(gè)線程檢查器

  • 主要 為了當(dāng)檢測條件時(shí)保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù)

  • 線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程噪漾,即線程是否被阻塞

使用

//初始化
NSCondition *condition = [[NSCondition alloc] init]

//一般用于多線程同時(shí)訪問硼砰、修改同一個(gè)數(shù)據(jù)源,保證在同一 時(shí)間內(nèi)數(shù)據(jù)源只被訪問欣硼、修改一次题翰,其他線程的命令需要在lock 外等待,只到 unlock 分别,才可訪問
[condition lock];

//與lock 同時(shí)使用
[condition unlock];

//讓當(dāng)前線程處于等待狀態(tài)
[condition wait];

//CPU發(fā)信號告訴線程不用在等待遍愿,可以繼續(xù)執(zhí)行
[condition signal];

底層分析

通過swift的Foundation源碼查看NSCondition的底層實(shí)現(xiàn)

open class NSCondition: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
    //初始化
    public override init() {
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(cond, nil)
    }
    //析構(gòu)
    deinit {
        pthread_mutex_destroy(mutex)
        pthread_cond_destroy(cond)

        mutex.deinitialize(count: 1)
        cond.deinitialize(count: 1)
        mutex.deallocate()
        cond.deallocate()
    }
    //加鎖
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    //解鎖
    open func unlock() {
        pthread_mutex_unlock(mutex)
    }
    //等待
    open func wait() {
        pthread_cond_wait(cond, mutex)
    }
    //等待
    open func wait(until limit: Date) -> Bool {
        guard var timeout = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    }
    //信號,表示等待的可以執(zhí)行了
    open func signal() {
        pthread_cond_signal(cond)
    }
    //廣播
    open func broadcast() {
        // 匯編分析 - 猜 (多看多玩)
        pthread_cond_broadcast(cond) // wait  signal
    }
    open var name: String?
}

其底層也是對下層pthread_mutex的封裝

  • NSCondition是對mutexcond的一種封裝(cond就是用于訪問和操作特定類型數(shù)據(jù)的指針)

  • wait操作會(huì)阻塞線程耘斩,使其進(jìn)入休眠狀態(tài)沼填,直至超時(shí)

  • signal操作是喚醒一個(gè)正在休眠等待的線程

  • broadcast會(huì)喚醒所有正在等待的線程

8、NSConditionLock

NSConditionLock是條件鎖括授,一旦一個(gè)線程獲得鎖坞笙,其他線程一定等待

相比NSConditionLock而言岩饼,NSCondition使用比較麻煩,所以推薦使用NSConditionLock薛夜,其使用如下

//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];

//表示 conditionLock 期待獲得鎖籍茧,如果沒有其他線程獲得鎖(不需要判斷內(nèi)部的 condition) 那它能執(zhí)行此行以下代碼,如果已經(jīng)有其他線程獲得鎖(可能是條件鎖梯澜,或者無條件 鎖)寞冯,則等待,直至其他線程解鎖
[conditionLock lock]; 

//表示如果沒有其他線程獲得該鎖晚伙,但是該鎖內(nèi)部的 condition不等于A條件吮龄,它依然不能獲得鎖,仍然等待咆疗。如果內(nèi)部的condition等于A條件漓帚,并且 沒有其他線程獲得該鎖,則進(jìn)入代碼區(qū)午磁,同時(shí)設(shè)置它獲得該鎖尝抖,其他任何線程都將等待它代碼的 完成,直至它解鎖迅皇。
[conditionLock lockWhenCondition:A條件]; 

//表示釋放鎖昧辽,同時(shí)把內(nèi)部的condition設(shè)置為A條件
[conditionLock unlockWithCondition:A條件]; 

// 表示如果被鎖定(沒獲得 鎖),并超過該時(shí)間則不再阻塞線程喧半。但是注意:返回的值是NO,它沒有改變鎖的狀態(tài)奴迅,這個(gè)函 數(shù)的目的在于可以實(shí)現(xiàn)兩種狀態(tài)下的處理
return = [conditionLock lockWhenCondition:A條件 beforeDate:A時(shí)間];

//其中所謂的condition就是整數(shù)青责,內(nèi)部通過整數(shù)比較條件

NSConditionLock挺据,其本質(zhì)就是NSCondition + Lock,以下是其swift的底層實(shí)現(xiàn)脖隶,

open class NSConditionLock : NSObject, NSLocking {
    internal var _cond = NSCondition()
    internal var _value: Int
    internal var _thread: _swift_CFThreadRef?
    
    public convenience override init() {
        self.init(condition: 0)
    }
    
    public init(condition: Int) {
        _value = condition
    }

    open func lock() {
        let _ = lock(before: Date.distantFuture)
    }

    open func unlock() {
        _cond.lock()
        _thread = nil
        _cond.broadcast()
        _cond.unlock()
    }
    
    open var condition: Int {
        return _value
    }

    open func lock(whenCondition condition: Int) {
        let _ = lock(whenCondition: condition, before: Date.distantFuture)
    }

    open func `try`() -> Bool {
        return lock(before: Date.distantPast)
    }
    
    open func tryLock(whenCondition condition: Int) -> Bool {
        return lock(whenCondition: condition, before: Date.distantPast)
    }

    open func unlock(withCondition condition: Int) {
        _cond.lock()
        _thread = nil
        _value = condition
        _cond.broadcast()
        _cond.unlock()
    }

    open func lock(before limit: Date) -> Bool {
        _cond.lock()
        while _thread != nil {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
         _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
        _cond.lock()
        while _thread != nil || _value != condition {
            if !_cond.wait(until: limit) {
                _cond.unlock()
                return false
            }
        }
        _thread = pthread_self()
        _cond.unlock()
        return true
    }
    
    open var name: String?
}

通過源碼可以看出

  • NSConditionLockNSCondition的封裝

  • NSConditionLock可以設(shè)置鎖條件扁耐,即condition值,而NSCondition只是信號的通知

調(diào)試驗(yàn)證

以下面代碼為例产阱,調(diào)試NSConditionLock底層流程

- (void)cjl_testConditonLock{
    // 信號量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1]; // conditoion = 1 內(nèi)部 Condition 匹配
        // -[NSConditionLock lockWhenCondition: beforeDate:]
        NSLog(@"線程 1");
         [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2];
        sleep(0.1);
        NSLog(@"線程 2");
        // self.myLock.value = 1;
        [conditionLock unlockWithCondition:1]; // _value = 2 -> 1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"線程 3");
       [conditionLock unlock];
    });
}
  • conditionLock部分打上響應(yīng)斷點(diǎn)婉称,運(yùn)行(需要在真機(jī)上運(yùn)行:模擬器上運(yùn)行的是Intel指令,而真機(jī)上運(yùn)行的是arm指令)

    調(diào)試驗(yàn)證-1

  • 斷住构蹬,開啟匯編調(diào)試


    調(diào)試驗(yàn)證-2
  • register read 讀取寄存器王暗,其中 x0是接收者 selfx1cmd

    調(diào)試驗(yàn)證-3

  • objc_msgSend處加斷點(diǎn)庄敛,再次讀寄存器 x0 -- register read x0俗壹,此時(shí)執(zhí)行到了[conditionLock lockWhenCondition:2];

    調(diào)試驗(yàn)證-4

  • 讀x1,即 register read x1 ,然后發(fā)現(xiàn)讀不出來藻烤,因?yàn)閤1存儲(chǔ)的是sel绷雏,并不是對象類型头滔,可以通過進(jìn)行強(qiáng)轉(zhuǎn)為SEL讀取

    調(diào)試驗(yàn)證-5

  • 加符號斷點(diǎn)-[NSConditionLock lockWhenCondition:]、-[NSConditionLock lockWhenCondition:beforeDate:],然后查看bl涎显、b等跳轉(zhuǎn)

    • 讀取寄存器 x0坤检、x2是當(dāng)前的lockWhenCondition:beforeDate:的參數(shù),實(shí)際走的是[conditionLock lockWhenCondition:1];
      調(diào)試驗(yàn)證-6
    • 通過匯編可知期吓,x2移動(dòng)到了x21
      調(diào)試驗(yàn)證-7

      到這里后早歇,我們調(diào)試的目的主要有兩個(gè):NSCondition + lock 以及condition與value的值匹配

NSCondition + lock驗(yàn)證

  • 繼續(xù)執(zhí)行,在bl處斷住讨勤,讀取寄存器 x0 缺前,此時(shí)是跳轉(zhuǎn)至NSCondition

    調(diào)試驗(yàn)證-8

  • 讀取 x1,即 po (SEL)0x00000001c746e484

    調(diào)試驗(yàn)證-9

所以可以驗(yàn)證NSConditionLock在底層調(diào)用的是NSConditionlock方法

condition與value的值匹配

  • 繼續(xù)執(zhí)行悬襟,跳到ldr 衅码,即通過一個(gè)方法,拿到了 condition 2 的屬性值脊岳,存儲(chǔ)到x8

    • register read x19
    • po (SEL)0x0000000283d0d220 -- x19的地址+0x10


      調(diào)試驗(yàn)證-10
    • register read x8逝段,此時(shí)的x8中存儲(chǔ)的是 2


      調(diào)試驗(yàn)證-11
    • cmp x8, x21,意思是將 x8和 x21匹配割捅,即 2 和 1匹配奶躯,并不匹配
      調(diào)試驗(yàn)證-12
  • 第二次來到cmp x8, x21,此時(shí)的x8亿驾、x21 是匹配的 嘹黔,即[conditionLock lockWhenCondition:2];

    調(diào)試驗(yàn)證-13

    此時(shí)是x8 和 x21 是匹配的,通過斷點(diǎn)也可以體現(xiàn)
    調(diào)試驗(yàn)證-14

demo分析匯總

  • 線程 1 調(diào)用[NSConditionLock lockWhenCondition:]莫瞬,此時(shí)此刻因?yàn)椴粷M足當(dāng)前條件儡蔓,所以會(huì)進(jìn)入 waiting 狀態(tài),當(dāng)前進(jìn)入到 waiting 時(shí)疼邀,會(huì)釋放當(dāng)前的互斥鎖喂江。

  • 此時(shí)當(dāng)前的線程 3 調(diào)用[NSConditionLock lock:],本質(zhì)上是調(diào)用 [NSConditionLock lockBeforeDate:]旁振,這里不需要比對條件值获询,所以線程 3 會(huì)打印

  • 接下來線程 2 執(zhí)行[NSConditionLock lockWhenCondition:],因?yàn)?code>滿足條件值拐袜,所以線程2 會(huì)打印吉嚣,打印完成后會(huì)調(diào)用[NSConditionLock unlockWithCondition:],這個(gè)時(shí)候將value 設(shè)置為 1,并發(fā)送 boradcast, 此時(shí)線程 1 接收到當(dāng)前的信號蹬铺,喚醒執(zhí)行并打印尝哆。

  • 自此當(dāng)前打印為 線程 3->線程 2 -> 線程 1

  • [NSConditionLock lockWhenCondition:];這里會(huì)根據(jù)傳入的 condition 值和 Value 值進(jìn)行對比,如果不相等丛塌,這里就會(huì)阻塞较解,進(jìn)入線程池畜疾,否則的話就繼續(xù)代碼執(zhí)行[NSConditionLock unlockWithCondition:]: 這里會(huì)先更改當(dāng)前的 value 值,然后進(jìn)行廣播印衔,喚醒當(dāng)前的線程

性能總結(jié)

  • OSSpinLock自旋鎖由于安全性問題啡捶,在iOS10之后已經(jīng)被廢棄,其底層的實(shí)現(xiàn)用os_unfair_lock替代

    • 使用OSSpinLock及所示奸焙,會(huì)處于忙等待狀態(tài)

    • os_unfair_lock是處于休眠狀態(tài)

  • atomic原子鎖自帶一把自旋鎖瞎暑,只能保證setter、getter時(shí)的線程安全与帆,在日常開發(fā)中使用更多的還是nonatomic修飾屬性

    • atomic:當(dāng)屬性在調(diào)用setter了赌、getter方法時(shí),會(huì)加上自旋鎖osspinlock玄糟,用于保證同一時(shí)刻只能有一個(gè)線程調(diào)用屬性的讀或?qū)懀?code>避免了屬性讀寫不同步的問題勿她。由于是底層編譯器自動(dòng)生成的互斥鎖代碼,會(huì)導(dǎo)致效率相對較低

    • nonatomic:當(dāng)屬性在調(diào)用setter阵翎、getter方法時(shí)逢并,不會(huì)加上自旋鎖,即線程不安全郭卫。由于編譯器不會(huì)自動(dòng)生成互斥鎖代碼砍聊,可以提高效率

  • @synchronized在底層維護(hù)了一個(gè)哈希表進(jìn)行線程data的存儲(chǔ),通過鏈表表示可重入(即嵌套)的特性贰军,雖然性能較低玻蝌,但由于簡單好用,使用頻率很高

  • NSLock词疼、NSRecursiveLock底層是對pthread_mutex的封裝

  • NSConditionNSConditionLock是條件鎖俯树,底層都是對pthread_mutex的封裝,當(dāng)滿足某一個(gè)條件時(shí)才能進(jìn)行操作寒跳,和信號量dispatch_semaphore類似

鎖的使用場景

  • 如果只是簡單的使用聘萨,例如涉及線程安全竹椒,使用NSLock即可

  • 如果是循環(huán)嵌套童太,推薦使用@synchronized,主要是因?yàn)槭褂?code>遞歸鎖的 性能 不如 使用@synchronized的性能(因?yàn)樵?code>synchronized中無論怎么重入胸完,都沒有關(guān)系书释,而NSRecursiveLock可能會(huì)出現(xiàn)崩潰現(xiàn)象)

  • 循環(huán)嵌套中,如果對遞歸鎖掌握的很好赊窥,則建議使用遞歸鎖爆惧,因?yàn)樾阅芎?/p>

  • 如果是循環(huán)嵌套,并且還有多線程影響時(shí)锨能,例如有等待扯再、死鎖現(xiàn)象時(shí)芍耘,建議使用@synchronized

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市熄阻,隨后出現(xiàn)的幾起案子斋竞,更是在濱河造成了極大的恐慌,老刑警劉巖秃殉,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坝初,死亡現(xiàn)場離奇詭異,居然都是意外死亡钾军,警方通過查閱死者的電腦和手機(jī)鳄袍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吏恭,“玉大人拗小,你說我怎么就攤上這事∮:撸” “怎么了十籍?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長唇礁。 經(jīng)常有香客問我勾栗,道長,這世上最難降的妖魔是什么盏筐? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任围俘,我火速辦了婚禮,結(jié)果婚禮上琢融,老公的妹妹穿的比我還像新娘界牡。我一直安慰自己,他們只是感情好漾抬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布宿亡。 她就那樣靜靜地躺著,像睡著了一般纳令。 火紅的嫁衣襯著肌膚如雪挽荠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天平绩,我揣著相機(jī)與錄音圈匆,去河邊找鬼。 笑死捏雌,一個(gè)胖子當(dāng)著我的面吹牛跃赚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播性湿,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼纬傲,長吁一口氣:“原來是場噩夢啊……” “哼满败!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叹括,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤葫录,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后领猾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體米同,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年摔竿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了面粮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡继低,死狀恐怖熬苍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袁翁,我是刑警寧澤柴底,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站粱胜,受9級特大地震影響柄驻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜焙压,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一鸿脓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涯曲,春花似錦野哭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绰沥,卻和暖如春篱蝇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揪利。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工态兴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疟位。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像喘垂,于是被迫代替她去往敵國和親甜刻。 傳聞我的和親對象是個(gè)殘疾皇子雹嗦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344