本篇提綱
1茫船、鎖的簡(jiǎn)介
2近速、鎖的性能分析
3、synchronized實(shí)現(xiàn)分析
4均牢、synchronized中的SyncData結(jié)構(gòu)
5糠雨、StripedMap的數(shù)據(jù)結(jié)構(gòu)
6、synchronized的執(zhí)行流程
1.鎖的簡(jiǎn)介
我們?cè)谑褂枚嗑€程的時(shí)候徘跪,可能會(huì)遇到多個(gè)線程同時(shí)訪問(wèn)同一個(gè)數(shù)據(jù)甘邀,導(dǎo)致數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)不安全的問(wèn)題砂竖,所以就需要使用線程同步。而最常見的線程同步的方式就是加鎖
鹃答,以保證同一時(shí)間只有同一個(gè)線程在訪問(wèn)共享數(shù)據(jù)乎澄。
2.鎖的性能分析
我們通過(guò)代碼十萬(wàn)次循環(huán),在循環(huán)中進(jìn)行加鎖测摔,解鎖的方式置济,來(lái)看一下各種鎖對(duì)循環(huán)的時(shí)間影響。下面分別是真機(jī)和锋八,模擬器運(yùn)行的結(jié)果浙于。
真機(jī)是iPhone11 iOS 15,模擬器是iPhone11 iOS 15
通過(guò)運(yùn)行結(jié)果可以看到@synchronized
這種鎖挟纱,在真機(jī)和模擬器上的表現(xiàn)差別很大羞酗,真機(jī)上性能要比模擬器好一些。而@synchronized
也是我們最常用的鎖紊服,這篇文章主要就來(lái)研究下@synchronized
的數(shù)據(jù)結(jié)構(gòu)和內(nèi)部的具體實(shí)現(xiàn)檀轨。
3.synchronized實(shí)現(xiàn)分析
我們通過(guò)符號(hào)斷點(diǎn)的方式或者clang編譯一下,跟蹤到@synchronized
對(duì)應(yīng)的代碼是這兩句:objc_sync_enter
欺嗤,objc_sync_exit
参萄,我們?cè)谠创a中看一下這兩個(gè)方法的具體實(shí)現(xiàn)。
- objc_sync_enter
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
不存在煎饼,那么會(huì)走objc_sync_nil 方法
讹挎,進(jìn)一步看,這個(gè)方法是一個(gè)宏定義吆玖,然后是空實(shí)現(xiàn)筒溃。也就是說(shuō),如果是obj
為空沾乘,就什么都不做怜奖。如果
obj
存在,那么會(huì)走上邊的if
分支意鲸,這里邊包括了一個(gè)新的結(jié)構(gòu)體SyncData
烦周,我們后邊會(huì)詳細(xì)看下它的結(jié)構(gòu)尽爆。objc_sync_exit
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
方法在objc_sync_enter
和objc_sync_exit
中都有調(diào)用怎顾,而且這兩個(gè)方法中的代碼實(shí)現(xiàn)也非常的相似,都是去判斷obj
漱贱,為空就什么都不做槐雾,有值就去走id2data
方法,我們來(lái)具體看下這個(gè)方法的實(shí)現(xiàn)幅狮。點(diǎn)進(jìn)去發(fā)現(xiàn)大概有一百六十行左右募强,還挺多的株灸。
static SyncData* id2data(id object, enum usage why)
{
//1、傳入object擎值,從哈希表中獲取數(shù)據(jù)
//mutex_tt->os_unfair_lock 根據(jù)里面的文檔翻譯 是自旋鎖
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
//傳入object慌烧,從哈希表中獲得SyncData的地址。
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;
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;
//2鸠儿、在當(dāng)前線程中的tls中尋找
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: {
//鎖+1
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
//再存儲(chǔ)到tls中
break;
}
case RELEASE:
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
//鎖的個(gè)數(shù)減完之后為0了
if (lockCount == 0) {
// remove from fast cache
//刪除局部存儲(chǔ)
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
//對(duì)SyncData對(duì)象的threadCount進(jìn)行-1屹蚊,因?yàn)楫?dāng)前線程中的對(duì)象已經(jīng)解鎖了
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
//3、TLS中沒(méi)找到进每,在各自線程的緩存中查找
// 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];
//沒(méi)匹配到 跳過(guò)
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");
}
//這個(gè)部分的執(zhí)行和在TLS中的類似
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;
}
}
// 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;
//4、遍歷syncList田晚,如果無(wú)法遍歷嘱兼,證明當(dāng)前object的list不存在,需要?jiǎng)?chuàng)建贤徒。
for (p = *listp; p != NULL; p = p->nextData) {
//查到了對(duì)象
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
//對(duì)threadCount+1
OSAtomicIncrement32Barrier(&result->threadCount);
//跳轉(zhuǎn)至done
goto done;
}
//沒(méi)查到 記錄下object的位置
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// 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;
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.
//創(chuàng)建一個(gè)新的SyncData對(duì)象 并且添加到syncList中
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); //內(nèi)存對(duì)齊
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
//頭插法,新增節(jié)點(diǎn)總是在頭部
result->nextData = *listp;
*listp = result;
done:
lockp->unlock(); //內(nèi)部的線程安全
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_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;
}
SUPPORT_DIRECT_THREAD_KEYS:支持線程占存接奈,線程占存
TLS
哪雕。TLS
,線程局部存儲(chǔ)(Thread Local Storage,TLS)鲫趁,是操作系統(tǒng)為線程單獨(dú)提供的私有空間斯嚎,通常只有有限的容量。ACQUIRE
在方法objc_sync_enter
傳入的值挨厚,對(duì)lockCount進(jìn)行+1操作堡僻,并存儲(chǔ)。RELEASE
在方法objc_sync_exit
傳入的值疫剃,對(duì)lockCount進(jìn)行-1操作钉疫,并進(jìn)一步判斷l(xiāng)ockCount的值是不是為0,如果為0巢价,對(duì)threadCount進(jìn)行-1操作牲阁。done
對(duì)list中找到的object而在TLS或者cache沒(méi)有找到的對(duì)象,進(jìn)行TLS存儲(chǔ)壤躲,或者cache存儲(chǔ)城菊,并且進(jìn)行一些錯(cuò)誤判斷。-
鏈表頭插法
鏈表頭插法演示.jpg
4.synchronized中的SyncData結(jié)構(gòu)
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中又有一個(gè)
struct SyncData* nextData;
相同類型的指向下一個(gè)節(jié)點(diǎn)的一個(gè)next碉克,所以這是一個(gè)單向鏈表
凌唬,節(jié)點(diǎn)中存儲(chǔ)了下一個(gè)節(jié)點(diǎn)的地址。 - threadCount使用block塊的線程數(shù)
- recursive_mutex_t遞歸鎖漏麦,底層還是os_unfair_lock客税。
5.StripedMap的數(shù)據(jù)結(jié)構(gòu)
我們通過(guò)代碼看到SyncData
是從LIST_FOR_OBJ
中取出來(lái)的况褪,
SyncData **listp = &LIST_FOR_OBJ(object);
進(jìn)一步看LIST_FOR_OBJ
它的定義是
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
是一個(gè)宏,而sDataLists
是一個(gè)靜態(tài)表
static StripedMap<SyncList> sDataLists;
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
StripedMap
是哈希類型更耻,所以sDataLists
是一張靜態(tài)哈希表测垛,內(nèi)部存儲(chǔ)SyncData
,而SyncData
本身又是單鏈表秧均,所以StripedMap
是哈希表+單鏈表的結(jié)構(gòu)赐纱。
而StripedMap
解決哈希沖突的方法是通過(guò)拉鏈法
,就是如果計(jì)算的下標(biāo)已經(jīng)存儲(chǔ)了內(nèi)容熬北,那么會(huì)存儲(chǔ)到SyncData`的next中疙描,如果next還有內(nèi)容,會(huì)繼續(xù)往下找讶隐,直到找到可以存儲(chǔ)的位置起胰。
StripedMap
結(jié)構(gòu)示意圖
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
};
在真機(jī)分配了8個(gè)空間,模擬器分配64個(gè)巫延。當(dāng)把模擬器修改成1后效五,不同的對(duì)象來(lái)到id2data
時(shí),通過(guò)打印可以看到炉峰,當(dāng)沖突了會(huì)存到?jīng)_突位置的nextData
中畏妖。
6.Synchronized的執(zhí)行流程
通過(guò)上面的討論,可以整理出以下流程疼阔。
1戒劫、調(diào)用@ synchronized(object){}時(shí),相當(dāng)于調(diào)用了方法objc_sync_enter
和objc_sync_exit
婆廊。
2迅细、在objc_sync_enter
方法中和objc_sync_exit
方法中首先都是進(jìn)行對(duì)傳入的object
判斷,如果為nil
就什么都不做淘邻;
如果存在茵典,那么objc_sync_enter
和objc_sync_exit
都會(huì)調(diào)用方法id2data
,只不過(guò)方法objc_sync_enter
中傳的參數(shù)是ACQUIRE
宾舅,而objc_sync_exit
傳的是RELEASE
统阿,這正好對(duì)應(yīng)了id2data
方法中switch
分支的處理。
3筹我、id2data
中的邏輯是這樣:
3.1 首先判斷是否支持TLS扶平,如果支持從TLS中查找相關(guān)的object存儲(chǔ)信息,查到了崎溃,入到
switch(why)
的分支判斷蜻直,如果是ACQUIRE
盯质,那么鎖lockCount+1袁串,然后更新存儲(chǔ)概而,返回result
;
如果是RELEASE
囱修,那么鎖lockCount-1赎瑰,然后更新存儲(chǔ),再進(jìn)一步判斷l(xiāng)ockCount是不是0破镰,如果為0餐曼,threadCount-1操作,然后更新存儲(chǔ)鲜漩。3.2 如果從TLS中沒(méi)查到源譬,那么查
SyncCache
緩存,進(jìn)行緩存的遍歷孕似,如果查到了這個(gè)對(duì)象的緩存踩娘,進(jìn)入到switch(why)
的分支判斷,如果是ACQUIRE
喉祭,那么鎖lockCount+1养渴,然后更新存儲(chǔ),返回result
泛烙;
如果是RELEASE
理卑,那么鎖lockCount-1,然后更新存儲(chǔ)蔽氨,再進(jìn)一步判斷l(xiāng)ockCount是不是0藐唠,如果為0,threadCount-1操作鹉究,然后更新存儲(chǔ)中捆。3.3 如果緩存也沒(méi)查到,那么去遍歷object所在的listp中查找坊饶,如果查到了泄伪,進(jìn)行threadCount的處理,并且跳轉(zhuǎn)到
done
匿级。done
的操作是蟋滴,先進(jìn)行上面查找讀取的解鎖,然后進(jìn)行簡(jiǎn)單的錯(cuò)誤判斷痘绎。如果支持TLS津函,那么把信息更新到TLS中進(jìn)行存儲(chǔ)(這樣下次再來(lái)的時(shí)候,第一步就可以查到了)孤页,如果不支持尔苦,那么更新到cache中,下次進(jìn)來(lái)的時(shí)候第二步就可以查到了。然后返回result
允坚。3.4 如果list中也沒(méi)查到魂那,那么創(chuàng)建一個(gè)新的SyncData對(duì)象 并使用頭插法插入到鏈表中(這樣下次再來(lái)到list就可以查到返回了,然后執(zhí)行l(wèi)ist往緩存存儲(chǔ)的流程)稠项,并且返回
result
涯雅。