@synchronized
本質(zhì)是個遞歸鎖
伍玖,不需要程序員手動加解鎖窍箍,并且不會產(chǎn)生死鎖問題丽旅,因此在開發(fā)中的使用頻率比較高纺棺,下面我們來研究一下他的底層實現(xiàn)祷蝌。
一、底層調(diào)用實現(xiàn)
@synchronized
是個關鍵字米丘,沒法在代碼中直接跳轉(zhuǎn)查看定義罪郊,最直接的辦法就是打上斷點尚洽、看匯編:
代碼跑起來看匯編:
在匯編中腺毫,有兩個關鍵代碼潮酒,
objc_sync_enter
、objc_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_object
即OC對象
拇舀,它保存了被鎖定對象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沖突
饱须。
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é)
-
@synchronized()
是遞歸鎖号杠,同一線程可重入姨蟋,只是內(nèi)部有個持鎖計數(shù)器而已 - 進入@synchronized()代碼塊時會執(zhí)行
objc_sync_enter(id obj)
加鎖 - 核心方法是通過
id2data()
來獲取到對象鎖節(jié)點SyncData
3.1. 首先從當前線程的私有數(shù)據(jù)(快速緩存
)中查找
3.2. 從當前線程整體緩存
中查找,檢查已擁有鎖的線程緩存中是否有匹配的對象
3.3. 從全局靜態(tài)listp
對象鎖鏈表中查找悠砚,并更新線程緩存
- 退出@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终蒂。