鎖的性能排行
鎖的歸類
自旋鎖:線程反復檢查鎖變量是否可用。由于線程在這一過程中保持執行,因此是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯示釋放自旋鎖。自旋鎖避免了進程上下文的調度開銷,因此對于線程只會阻塞很短時間的場合是有效的。
互斥鎖:是一種用于多線程編程中,防止兩條線程同時對同一公共資源進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區而達成。
上圖中屬于互斥鎖的有:
- 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(@"當前車票已售罄");
}
// }
}
在沒有考慮到線程安全的情況我們運行其任務
- 這明顯這票數 有問題 票池 抽了瘋,不管三七二一的瞎胡 扯的反饋。
當用上了 @synchronized 完美的解決了問題
2、如何分析synchronize
那肯定是符號斷點,clang了
首先符號斷點打開 Debug
-> Debug Workflow
-> Always Show Disassembly
將斷點 打到 @synchronized 并運行 在匯編里我們找到了 兩個很重要的線索
- objc_sync_enter 函數
- objc_sync_exit 函數
我們 到此先記住這兩個函 這是可疑的兩個函數 下面在clang一下 @synchronized 看clang編譯器是怎樣實現的。
在main函數中寫一個 @synchronized
通過命令 得到 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
- 通過 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
中 查找,就需要對查找過程加鎖防止多線程查找導致數據異常。使用列表sDataLists
把SyncData
又做了一層封裝,元素是一個結構體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
緩存中刪除。未找到進入22、從線程緩存
SyncCache
中查找,如果找到,對鎖的計數減1
,更新緩存中的數據,如果當前對象對應的鎖計數為0了,直接將其從線程緩存SyncCache
中刪除。未找到進入33、從
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
,并寫入緩存.