@synchronized是比較常見的線程間同步鎖,其使用相當簡單:
@synchronized (self) {
NSLog(@"-----synchronized-----");
}
可在上述代碼synchronized行斷點,并通過xcode轉(zhuǎn)換為匯編代碼催式,或通過終端用clang命令翻譯源碼:
clang -x objective-c -rewrite-objc -isysroot /.../main.m
可看到 @synchronized 代碼塊調(diào)用了兩個函數(shù):
objc_sync_enter
objc_sync_exit
1. objc_sync_enter
必要時開辟一個關(guān)聯(lián)著obj的互斥遞歸鎖,進入與obj關(guān)聯(lián)的同步工作,當獲得鎖之后返回OBJC_SYNC_SUCCESS叼风。
// 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;
}
objc_sync_enter主要邏輯:
obj 不為空時,獲取SyncData *data
棍苹,取出data->mutex
進行加鎖无宿;
obj 為空時,執(zhí)行obj_sync_nil
枢里,通過源碼查看其實什么也沒有處理孽鸡。
2. 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;
}
objc_sync_exit 也是找到對應的 SyncData,然后進行解鎖栏豺。
可以看出彬碱,@synchronized 核心是一個互斥遞歸鎖。
objc_sync_enter 的過程主要是關(guān)于 SyncData 的創(chuàng)建奥洼、獲取及相關(guān)處理巷疼,SyncData結(jié)構(gòu)如下:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount;
recursive_mutex_t mutex;
} SyncData;
3. id2data
id2data方法中可分為6個基本操作,簡要過程如下:
static SyncData* id2data(id object, enum usage why) {
//// 步驟 1
//// 獲取listp溉卓,及對其操作的鎖lockp
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;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
//// 步驟 2
//// 從TLS中獲取SyncData
// ....
}
#endif
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
//// 步驟 3
//// 從cache中獲取SyncData
// ....
}
// 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();
{
//// 步驟 4
//// 在全局的listp中查找object對應的SyncData皮迟,并修改
// ....
}
//// 步驟 5
//// 創(chuàng)建新的SyncData搬泥,并頭插至listp鏈表
// malloc a new SyncData and add to list.
// XXX calling malloc with a global lock held is bad practice,
// might be worth releasing the lock, mallocing, and searching again.
// But since we never free these guys we won't be stuck in malloc very often.
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t();
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
//// 步驟 6
//// 在TLS、cache中存儲新創(chuàng)建的SyncData
}
return result;
}
具體實現(xiàn)可查蘋果源碼:https://opensource.apple.com/source/objc4/objc4-680/runtime/objc-sync.mm
步驟1 — 獲取listp伏尼,及對其操作的鎖lockp
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
// 兩個宏的定義
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
其中忿檩,StripedMap
是一個哈希表:
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
// 內(nèi)部有一個T類型的value值
struct PaddedT {
T value alignas(CacheLineSize);
};
// array來存儲PaddedT
PaddedT array[StripeCount];
// 哈希函數(shù)
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
// ...
StripedMap 內(nèi)部是一個容量為8的數(shù)組,存儲T類型的數(shù)據(jù)爆阶,當前存儲的就是SyncList
:
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
以上兩個宏的主要作用是燥透,通過哈希算法得出obj所在的SyncList
,進一步取出對應的data
和一把針對listp
的spinlock_t鎖
辨图。
步驟2 — 從TLS中獲取SyncData
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
// 檢查單獨線程的快速緩存
bool fastCacheOccupied = NO;
// 通過tls來獲取SyncData
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
// 校驗取出的data是否和此次的object一致
if (data->object == object) {
// Found a match in fast cache.
uintptr_t lockCount;
result = data;
// 獲取當前線程中對于object的加鎖次數(shù)班套,因為是遞歸鎖,所以存在多次加鎖
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: {
// ACQUIRE類型表示新增加了一次鎖
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
// RELEASE表示此次加鎖結(jié)束了
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
// 0代表當前線程已經(jīng)沒有針對object加鎖故河,此時thread_count需要減一
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 中快速查找 obj 對應的 SyncData吱韭,若找到,則根據(jù)傳入的參數(shù)ACQUIRE/RELEASE 來修改 lockCount 以及 threadCount鱼的,同時更新tls中的值理盆。
步驟3 — 從cache中獲取SyncData
如果tls中沒有找到對應的SyncData,會進入步驟3:
// 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;
// 遍歷cache查找object對應的cacheItem
// 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;
}
}
順帶看一下 SyncCacheItem凑阶、fetch_cache 的實現(xiàn):
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
static SyncCache *fetch_cache(bool create)
{
_objc_pthread_data *data;
data = _objc_fetch_pthread_data(create);
if (!data) return NULL;
if (!data->syncCache) {
if (!create) {
return NULL;
} else {
// 默認緩存容量為4
int count = 4;
data->syncCache = (SyncCache *)
calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
data->syncCache->allocated = count;
}
}
// Make sure there's at least one open slot in the list.
// 擴容
if (data->syncCache->allocated == data->syncCache->used) {
data->syncCache->allocated *= 2;
data->syncCache = (SyncCache *)
realloc(data->syncCache, sizeof(SyncCache)
+ data->syncCache->allocated * sizeof(SyncCacheItem));
}
return data->syncCache;
}
可以發(fā)現(xiàn)猿规,其實和步驟2類似,步驟3會從 SyncCache 中取出對應的 SyncData宙橱,之后進行步驟2中類似的處理姨俩。
步驟4 — 查找并修改全局listp中與object對應的SyncData
若cache還沒有創(chuàng)建,那么需要從全局的listp鏈表中尋找Syncdata师郑,如果這也沒找到环葵,但是找到了空結(jié)點,就對空結(jié)點進行賦值宝冕。
// 加鎖
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
// 在全局的listp鏈表中查找object對應的SyncData
for (p = *listp; p != NULL; p = p->nextData) {
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
// 找到說明當前線程是第一次對object進行加鎖积担,此時需要threadCount+1
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
// 如果當前結(jié)點的threadCount為0,即當前結(jié)點對應的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
// 如果找到一個沒有用的結(jié)點猬仁,對結(jié)點進行重新初始化,重新賦值
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;
result->threadCount = 1;
goto done;
}
}
其中先誉,可能會找到了空結(jié)點湿刽,這個空結(jié)點挺有意思。
步驟5 — 創(chuàng)建新的SyncData褐耳,并頭插至listp鏈表
緊接著上面的listp查找诈闺,如果listp沒有空結(jié)點,只能創(chuàng)建新的結(jié)點铃芦,并頭插至鏈表listp中:
// 鏈表中的結(jié)點都被占用雅镊,此時只能創(chuàng)建新的結(jié)點了
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);
// 頭插法插入到當前的listp鏈表中
result->nextData = *listp;
*listp = result;
步驟6 — 在TLS襟雷、cache中存儲新建的SyncData
done:
// 此時object對應的syncdata已經(jīng)創(chuàng)建完畢,并且存儲完成仁烹,對于多線程已經(jīng)沒有了風險耸弄,可以解鎖了
lockp->unlock();
if (result) {
...
#if SUPPORT_DIRECT_THREAD_KEYS
// 如果快速緩存即tls還沒有被占用,存儲快速緩存中
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
// 否則存儲到線程的緩存syncCache中
{
// Save in thread cache
// cache不存在的話需要先創(chuàng)建cache
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
id2data總結(jié)
關(guān)于 id2data 方法卓缰,其本質(zhì)就是一個找object對應的SyncData的過程计呈,先從tls即fast cache中找,再從線程的syncCache中找征唬,最后從全局的listp鏈表中找捌显,都找不到的話只能自己創(chuàng)建,然后存儲到對應的位置总寒。
從前面的代碼可以看到,在ios系統(tǒng)中摄闸,全局的哈希表容量為8:
在分析過 id2data 后善镰,回頭在看 objc_sync_enter、objc_sync_exit 就比較簡單了贪薪,也是找到對應的 SyncData媳禁,獲取到遞歸互斥鎖,然后進行加/解鎖画切。
4. @synchronized 注意事項
以下代碼意圖在多線程中對_testArray進行初始化操作竣稽,在執(zhí)行過程中出現(xiàn)了崩潰:
崩潰原因是同時有兩條線程執(zhí)行了賦值代碼時,導致原有的_testArray舊值被釋放了兩次霍弹。
我們對操作加@synchronized鎖毫别,仍然出現(xiàn)了崩潰:
主要原因是以_testArray為參數(shù)加鎖,當 _testArray 發(fā)生變化之后典格,在后續(xù)的線程和之前的線程中加鎖的對象已經(jīng)發(fā)生了變化岛宦,導致不同的線程取出了不一樣的syncData,也導致鎖的失效耍缴,從而引發(fā)了后續(xù)的崩潰問題砾肺。
如果我們針對self
進行加鎖,就可以避免這個問題防嗡。
參考文章:
https://juejin.cn/post/6903162266489880584
https://juejin.cn/post/6844904086161063944
http://www.reibang.com/p/a816e8cf3646