@synchronized底層探索&其他鎖

鎖的性能排行

鎖的性能排行.png

鎖的歸類

自旋鎖:線程反復檢查鎖變量是否可用。由于線程在這一過程中保持執行,因此是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯示釋放自旋鎖。自旋鎖避免了進程上下文的調度開銷,因此對于線程只會阻塞很短時間的場合是有效的。

互斥鎖:是一種用于多線程編程中,防止兩條線程同時對同一公共資源進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區而達成。
上圖中屬于互斥鎖的有:

  • NSLock
  • pthread_mutex
  • @synchronized

條件鎖:就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了。當資源分配到了,條件鎖打開,進程繼續運行
上圖中屬于條件鎖的有:

  • NSConfition
  • NSConditionLock

遞歸鎖:就是同一線程可以加鎖N次而不會引發死鎖
上圖中屬于遞歸鎖的有

  • NSRecursiveLock
  • pthread_mutex(recursive)

信號量(semaphore):是一種更高級的同步機制,互斥鎖可以說是semaphore在僅取值0/1時的特例。信號量可以有更多的取值空間,用來實現更加復雜的同步,而不單單是線程間的互斥。

其實基本的鎖就包括了三類,自旋鎖 互斥鎖 讀寫鎖,其他的比如條件鎖,遞歸鎖,信號量都是上層的封裝和實現!

引用:百度百科讀寫鎖

@synchronized

對于@synchronized 的使用大家都不陌生,但是它的底層實現是怎樣的呢?通過底層分析我們又能得到什么新的發現?下面廢話不多說直接探尋其底層。

如何進行探索(知道的可略過直接去看底層源碼分析)
1、 dome 準備
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 15;
    [self  testSaleTicket];
}
 
- (void)testSaleTicket{
    ///窗口 1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    ///窗口 2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    ///窗口 3
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket{
//     @synchronized (self) {
        
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"當前余票還剩:%ld張",self.ticketCount);
            
        }else{
            NSLog(@"當前車票已售罄");
        }
//     }
 }

在沒有考慮到線程安全的情況我們運行其任務


截屏2020-11-17 下午2.25.41.png
  • 這明顯這票數 有問題 票池 抽了瘋,不管三七二一的瞎胡 扯的反饋。

當用上了 @synchronized 完美的解決了問題


截屏2020-11-17 下午2.36.29.png
2、如何分析synchronize

那肯定是符號斷點,clang了
首先符號斷點打開 Debug -> Debug Workflow -> Always Show Disassembly

符號斷點打開

將斷點 打到 @synchronized 并運行 在匯編里我們找到了 兩個很重要的線索
匯編線索1

匯編線索2
  • objc_sync_enter 函數
  • objc_sync_exit 函數

我們 到此先記住這兩個函 這是可疑的兩個函數 下面在clang一下 @synchronized 看clang編譯器是怎樣實現的。
在main函數中寫一個 @synchronized


截屏2020-11-17 下午3.02.33.png

通過命令 得到 mian.cpp

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
截屏2020-11-17 下午3.16.03.png
  • 通過 clang我們 也發現了上面的兩個函數 是一模一樣的,證明上面的兩個函數 正是 我們要研究的。

找到 objc_sync_enter 和 objc_sync_exit 所在的庫
下符號斷點


下符號斷點找到所在的庫
  • 此時我們知道了 objc_sync_enter 函數 是在 libojc 中掉起的。


    截屏2020-11-17 下午3.25.43.png
  • objc_sync_exit 函數 也是由 libobjc 中調起的

到這里我們也就知道了@synchronized 底層 是由 objc_sync_enter 和objc_sync_exit 兩個重要的函數組合而成 他們來自 libobjc 動態庫。也就找到 程序的入口 分析的入口。

objc_sync_enter&objc_sync_exit 函數分析

找到objc4源碼 并定位到當前函數

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;
}
  • 從這里可以看到 如果obj為真的話 通過id2data函數 獲取一個SyncData 對象,并將此對象里面的 mutex 的屬性 上鎖

我們看 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;
  • 可以看到SyncData是一個結構體,里面包含一個指向下一個SyncData的指針nextData,可以看出SyncData是鏈表中的一個節點。
  • 包含object 將其類型進行了偽裝,其實它就是我們傳進來的 object。
  • 里面還有一個 threadCount,通過注釋我們可以詳細的看到 使用此塊的線程數。
  • 還有一把鎖,從這把鎖的定義來看 它是一個遞歸互斥類型

來到 id2data函數看里面如何獲取到SyncData的對象的
由于函數太長我們拆分幾大塊來看

第一步: 判斷是否支持tls緩存,從tls緩存中獲取obj的相關信息

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
    //檢查每個線程單條目快速緩存是否匹配對象
    // Check per-thread single-entry fast cache for matching object
    ///默認沒找到
    bool fastCacheOccupied = NO;
    ///從線程中讀取數據  (tls: (Thread Local Storage) 線程本地存儲)
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
   /// 找到了 設置為 YES
        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);
            /// 如果使用此塊兒的線程總數 或者 當前線程被鎖次數 都小于等于0 那么這時候bug
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {///進行中
                lockCount++;///將當前線程被鎖次數+1
                 ///更新線程緩存的任務數
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:/// 釋放中
                lockCount--;///將當前線程被鎖次數 -1
                ///更新線程緩存的任務數
                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

tls,Thread Local Storage,線程局部儲存,它是操作系統為線程單獨提供的私有空間,通常只有有限的容量。
百度百科:線程局部存儲

  • tls讀取數據,如果找到了并且和當前被鎖對象一樣,獲取當前 線程 被鎖幾回的lockCount。
  • 如當前 是 ACQUIRE (也就是 objc_sync_enter調用的)那說明在當前線程上對象又被鎖了一次,鎖的次數加+1。 更新tls中存儲的obj信息。并返回
  • 如當前 是 RELEASE (也就是 objc_sync_exit)發起的調用,那說明 在當前線程上的被鎖任務應該 -1 。更新tls中存儲的obj信息。并返回
  • 如在tls中并未找到,那么進入第二步

第二步:在線程緩存中SyncCache中查找是否存在obj的數據信息

#endif
  
   /// //檢查已擁有鎖的每個線程緩存是否匹配對象
    // 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;
            /// 如果 當前對象所關聯的 線程總數 小于等于0
            /// 或 當前對象所關聯的線程 鎖任務的個數小于等于0 程序bug
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE: ///進行中
                item->lockCount++; ///當前線程任務數+1
                break;
            case RELEASE:///釋放中
                item->lockCount--; ///當前線程任務數 -1
                if (item->lockCount == 0) { ///當前線程加鎖任務 為 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; ///返回
        }
    }

  • 從線程緩存中遍歷查找 和當前傳進的對象對應的線程緩存。 如找到了 拿到 當前線程的緩存對象SyncCacheItem 。

  • 如當前 是 ACQUIRE (也就是 objc_sync_enter調用的)那說明在當前線程上對象又被鎖了一次,鎖的次數(lockCount)加+1。

  • 如當前 是 RELEASE (也就是 objc_sync_exit)發起的調用,那說明 在當前線程上的被鎖任務次數標識(lockCount)應該 -1 。 如果當前線程上的任務數為0 那么移除線程緩存

  • 如在線程緩存中也沒有那么進入第三步

這里看一下 緩存結構(SyncCache )及 緩存對象結構(SyncCacheItem)

///線程緩存
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
///緩存對象item
typedef struct {
SyncData *data;
unsigned int lockCount;  // number of times THIS THREAD locked >this block
} SyncCacheItem;

第三步:使用列表 sDataLists中查找對象,并做處理

    // 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.
    ///線程緩存沒有找到任何東西。,需要遍歷每個線程,沿著nextData遞歸查找
    ///上鎖
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
      
        ///遍歷跟當前object相關的 所有線程任務
        for (p = *listp; p != NULL; p = p->nextData) {
            ///再次判斷是否是當前 object
            if ( p->object == object ) {
                result = p;//找到賦值
                 //原子操作 可能會和 并發 釋放 沖突
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;//跳出
            }
            ///沒找到與當前objc關聯的鎖任務線程 更新第一個沒有使用的線程
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
        //當前沒有與對象關聯的SyncData
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        //發現一個未使用的,就使用它
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;///將當前對象存入 object
            result->threadCount = 1;//只有一個線程加鎖
            goto done;
        }
    }

    //分配一個新的SyncData并添加到列表
    // 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.
    //分配一個新的SyncData并添加到列表。
    // XXX用持有的全局鎖分配內存是不好的做法,
    //可能值得釋放鎖、重新分配和搜索。
    //但由于我們從來沒有釋放這些,我們就不會經常陷入分配的困境。
    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;
    
 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) {
            // Save in fast thread cache ///存入 tls
            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++;
        }
    }

    return result;
}
  • 在列表sDataLists中 查找,就需要對查找過程加鎖防止多線程查找導致數據異常。使用列表 sDataListsSyncData又做了一層封裝,元素是一個結構體SyncList.

這里我們回到最上面看一下 *listp

 ///跟當前對象關聯的所有的被鎖線程中的鎖任務的狀態
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
 ///跟當前對象關聯的所有的被鎖線程數據

SyncData **listp = &LIST_FOR_OBJ(object);
///進入 LOCK_FOR_OBJ 發現是一個宏
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
///sDataLists 是一個靜態的 map 泛型為 SyncList 也就是key為object指針,value為SynLlist
static StripedMap<SyncList> sDataLists;

struct SyncList {
SyncData *data;
spinlock_t lock;

constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
  • 如找到,解鎖,將數據寫入tls ,寫入線程緩存,并返回數據
  • 如未找到,創建一個新的SyncData放入sDataLists中,并存入tls線程緩存中然后返回

看完了objc_sync_enter 下面看 objc_sync_exit 鎖的釋放

// End synchronizing on 'obj'. ///根據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;
}

上面我們已經統一的分析了id2data函數,這里傳進的是RELEASE
下面總結objc_sync_exit 函數 的id2data做了什么事情

  • 1、先從tls緩存中查找,如果找到,對鎖的計數(lockCount)減1,更新緩存中的數據,如果當前對象對應的鎖計數為0了,直接將其從tls緩存中刪除。未找到進入2

  • 2、從線程緩存SyncCache中查找,如果找到,對鎖的計數減1,更新緩存中的數據,如果當前對象對應的鎖計數為0了,直接將其從線程緩存SyncCache中刪除。未找到進入3

  • 3、從sDataLists查找,找到的話,直接將其置為nil

總結

  • synchronized底層我們看到了有對同一條線程上的 加鎖任務計數lockCount。有 使用此塊 的線程數的統計 threadCount
    還看到了 SyncData對象中 的 recursive_mutex_t。
    由此可以下結論synchronized 是一把 遞歸互斥鎖,
  • synchronized 進入代碼塊的入口為objc_sync_enter,出口為objc_sync_enter
  • 如果@synchronized(nil)傳入的為nil那么鎖將不起任何作用
    核心處理邏輯:
  • 如支持tls緩存,就從tls緩存中查找對象SyncData,找到對lockCount進行相應操作。
  • 如果不支持tls緩存,或者從tls緩存中未找到,就從線程緩存SyncCache中查找,同樣如找到 就對lockCount進行相應操作。
  • 如緩存中沒有找到,就從sDataLists鏈表中查找,找到后進行相關操作,并寫入tls緩存和線程緩存SyncCache.
  • 都沒找到,創建一個節點,將對象鎖SyncData插入sDataLists,并寫入緩存.
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容