synchronizd是Objective-C中的一個語法糖,用于給某個對象加鎖,因為使用起來簡單方便,所以使用頻率很高。然而,濫用@synchronizd很容易導致代碼效率低下。本篇博客旨在結合@synchronizd底層實現源碼并剖析其實現原理,這樣可以更好的讓我們在適合的情景使用@synchronizd。
@synchronizd本質上是一個編譯器標識符,在Objective-C層面看不其任何信息。因此可以通過clang -rewrite-objc指令來獲得@synchronizd的C++實現代碼。示例代碼如下:
int main(int argc, const char * argv[]) {
NSString *obj = @"Iceberg";
@synchronized(obj) {
NSLog(@"Hello,world! => %@" , obj);
}
}
int main(int argc, const char * argv[]) {
NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;
{
id _rethrow = 0;
id _sync_obj = (id)obj;
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((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);
} 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);
}
}
}
通過分析C++代碼可以看到@sychronized的實現主要依賴于兩個函數:objc_sync_enter和objc_sync_exit。此外還有try{}catch{}語句用于捕捉@sychronized{}語法塊中代碼執行過程中出現的異常。
我們發現objc_sync_enter函數是在try語句之前調用,參數為需要加鎖的對象。因為C++中沒有try{}catch{}finally{}語句,所以不能在finally{}調用objc_sync_exit函數。因此objc_sync_exit是在_SYNC_EXIT結構體中的析構函數中調用,參數同樣是當前加鎖的對象。這個設計很巧妙,原因在_SYNC_EXIT結構體類型的_sync_exit是一個局部變量,生命周期為try{}語句塊,其中包含了@sychronized{}代碼需要執行的代碼,在代碼完成后,_sync_exit局部變量出棧釋放,隨即調用其析構函數,進而調用objc_sync_exit函數。即使try{}語句塊中的代碼執行過程中出現異常,跳轉到catch{}語句,局部變量_sync_exit同樣會被釋放,完美的模擬了finally的功能。
接下來,在蘋果公開的源代碼文件objc-sync.mm中找到objc_sync_enter和objc_sync_exit這兩個函數的實現,一窺其中的奧秘。
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object; //當前加鎖的對象
int32_t threadCount; //使用對object加鎖的線程個數
recursive_mutex_t mutex; //遞歸互斥鎖
} SyncData;
typedef struct {
SyncData *data;
unsigned int lockCount; //表示當前線程對object對象加鎖次數
} SyncCacheItem;
typedef struct SyncCache {
unsigned int allocated;
unsigned int used;
SyncCacheItem list[0];
} SyncCache;
/*
Fast cache: two fixed pthread keys store a single SyncCacheItem.
This avoids malloc of the SyncCache for threads that only synchronize
a single object at a time.
SYNC_DATA_DIRECT_KEY == SyncCacheItem.data
SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
*/
struct SyncList {
SyncData *data;
spinlock_t lock;
SyncList() : data(nil) { }
};
// 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;
上述代碼是一些相關的數據結構,下面分別進行介紹:
SyncData結構體中有四個成員變量,其中object指針變量指向當前加鎖對象,threadCount表示對object加鎖的線程個數,mutex是一個遞歸互斥鎖,意味著可以對object進行多次加鎖,其具體作用后面會提到。
SyncCacheItem結構體中有兩個成員變量,其中data是SyncData結構體類型的指針,lockCount表示當前線程對當前結構體對象加鎖次數,其實就是對加鎖對象object的加鎖次數。我們可以看到SyncCacheItem與SyncData是一對一關系,SyncCacheItem只是對SyncData進行了再次封裝以便于緩存,具體使用見后文。
SyncCache結構體中有三個成員變量,其中維護了一個SyncCacheItem類型的數組,allocated和used則分別表示當前分配的SyncCacheItem數組中的總個數和已經使用的個數。這個結構體與線程是一對一的關系,用于存儲當前線程已加鎖對象對應的SyncCacheItem結構體,因為一個線程可以對同一個對象多次加鎖,所以通過引入緩存SyncCache可以提高效率,具體使用見后文。
SyncList結構體中有兩個成員變量和一個構造函數,其中data是SyncData結構體類型的指針,lock是一個自旋鎖。
sDataLists是一個全局StripedMap哈希列表,其中value為SyncList對象,key為加鎖對象object指針進行hash后的值。StripedMap是一個C++模板類,其實現代碼如下所示:
template<typename T>
class StripedMap {
enum { CacheLineSize = 64 };
#if TARGET_OS_EMBEDDED
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;
}
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];
}
#if DEBUG
StripedMap() {
// Verify alignment expectations.
uintptr_t base = (uintptr_t)&array[0].value;
uintptr_t delta = (uintptr_t)&array[1].value - base;
assert(delta % CacheLineSize == 0);
assert(base % CacheLineSize == 0);
}
#endif
};
上述代碼中,由于自己對C++模板類不熟悉,所以只能看個大概。其中有兩個值得注意的地方,其中StripeCount表示哈希數組的長度,如果是嵌入式系統值為8,否則值為64,也就意味著哈希數組最大長度為64;另外indexForPointer函數是用于計數哈希下標的函數,算法不難,但是很巧妙,值得學習。
下面開始分析相關的函數實現,首先找到@sychronized直接調用的兩個函數:objc_sync_enter和objc_sync_exit,代碼如下:
// 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;
}
// 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;
}
不難發現,上述代碼都調用了id2data函數來獲取一個與obj對應的SyncData對象,然后使用該對象中的遞歸互斥鎖分別進行加鎖與解鎖。至此@sychronized的大致實現過程已經很清晰了,本質上是為一個對象分配一把遞歸互斥鎖,可以也是為什么可以反復使用@sychronized對同一個對象進行加鎖的原因。那么@sychronized是如果管理這把互斥鎖,以及是如何處理多個線程對同一個對象進行多次加鎖的情況?很明顯,一切奧秘都藏在id2data函數中,其代碼如下所示:
- 注:為了描述方便,下面將id2data函數的形參object描述為同步對象obejct。
static SyncData* id2data(id object, enum usage why)
{
//從全局哈希表sDataLists中獲取object對應的SyncList對象
//lockp指針指向SyncList對象中自旋鎖
//listp指向一條SyncData鏈表
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
//對于同一個線程來說,有兩種緩存方式:
//第一種:快速緩存(fastCache),適用于一個線程一次只對一個對象加鎖的情況,用宏SUPPORT_DIRECT_THREAD_KEYS來標識
//這種情況意味著同一時間內,線程緩存中只有一個SyncCacheItem對象,鍵值SYNC_DATA_DIRECT_KEY和SYNC_COUNT_DIRECT_KEY分別對應SyncCacheItem結構體中的SyncData對象和lockCount.
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
//用于標識當前線程的是否已使用fastCache
bool fastCacheOccupied = NO;
//直接調用tls_get_direct函數獲取SyncData對象
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
//標識fastCache已被使用
fastCacheOccupied = YES;
//比較fastCache中的SyncData對象中的object與當前同步對象object是否為同一個對象
if (data->object == object) {
// Found a match in fast cache.
//fastCache中的對象恰好是當前同步對象object,則后續處理直接使用fastCache中SyncData對象
uintptr_t lockCount;
result = data;
//獲取當前線程對應當前SyncData對象已經加鎖的次數
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
//無效的SyncData對象
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);
//已加鎖次數為0,表示當前線程對當前同步對象object達到鎖平衡,因此不需要再持有當前同步對象。
if (lockCount == 0) {
// remove from fast cache
//將對應的SyncData對象從線程緩存中移除
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
//此函數為原子操作函數,用于對32位的threadCount整形變量執行減一操作,且確保線程安全。因為可能存在同一時間多個線程對一個threadCount進行加減操作,避免出現多線程競爭。不同于lockCount,threadCount是多個線程共享的一個變量,用于記錄對一個對象加鎖的線程個數,threadCount對應的SyncData對象除了線程緩存中持有之外,還存在于全局哈希表sDataLists中,sDataLists哈希表是多個線程共享的數據結構,因此存在多線程訪問的可能。而lockCount則與線程一一對應且存儲在線程的緩存區中,不存在多線性讀寫問題,因此不需要加鎖。
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing
break;
}
return result;
}
}
#endif
// Check per-thread cache of already-owned locks for matching object
//這是第二章緩存方式:使用SyncCache結構體來維護一個SyncCacheItem數組,這樣一個線程就可以處理對多個同步對象。值得注意的是SyncCache與線程也是一對一的關系。
//獲取當前線程緩存區中的SyncCache對象
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
//遍歷SyncCache對象中的SyncCacheItem數組,匹配當前同步對象object
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
if (item->data->object != object) continue;
// Found a match.
//當前同步對象object已存在的SyncCache中
//獲取對應的SyncData對象
result = item->data;
//無效的SyncData對象
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
//后續操作同fastCache一樣,參考fastCache的注釋
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.
//如果當前線程中的緩存中沒有找到當前同步對象對應的SyncData對象,則在全局哈希表中查找
//因為全局哈希表是多個線程共享的數據結構,因此需要進行加鎖處理
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
//遍歷當前同步對象obejct在全局哈希表中的SyncData鏈表。這里之所以使用鏈表,是因為哈希表的hash算法不能確保hash的唯一性,存在多個對象對應一個hash值的情況。
for (p = *listp; p != NULL; p = p->nextData) {
//哈希表中存在對應的SyncData對象
if ( p->object == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
//此函數為原子操作函數,確保線程安全,用于對32位的threadCount整形變量執行加一操作,表示占用當前同步對象的線程數加1。
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
//用于標記一個空閑的SyncData對象
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
//由于此時同步對象object沒有對應的SyncData對象,因此RELEASE與CHECK都屬于無效操作
if ( (why == RELEASE) || (why == CHECK) )
goto done;
// an unused one was found, use it
//如果沒有找到匹配的SyncData對象且存在空閑的SyncData對象,則直接使用,不需要創建新的SyncData,以提高效率。
if ( firstUnused != NULL ) {
result = firstUnused;
//關聯當前同步對象
result->object = (objc_object *)object;
//重置占用線程為1
result->threadCount = 1;
goto done;
}
}
// 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.
//到這一步說明需要新建一個SyncData對象
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
//創建遞歸互斥鎖
new (&result->mutex) recursive_mutex_t();
//以“入棧”的方式加入當前同步對象object對應的SyncData鏈表
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.
//只有ACQUIRE才需要新建SyncData對象
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");
//fastCache緩存模式
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
//直接緩存新建的SyncData對象
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
//設置加鎖次數為1
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
//SyncCache緩存模式,則直接加入SyncCacheItem數組中
{
// 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;
}
通過上述代碼的注釋,id2data函數的功能已經大致清晰。id2data函數主要是用于管理同步對象object與線程之間的關聯。不論是ACQUIRE、RELEASE還是CHECK操作,都會先從當前線程的緩存中去獲取對應的SyncData對象。如果當前線程的緩存區中不存在,那么再從全局的哈希數組中查找,查看其它線程是否已經占用過當前同步對象object。如果還是沒有,那么就新建一個與之對應的SyncData對象,分別加入全局哈希表和當前線程緩存中。
至此,@synchronized的實現原理已經剖析結束,其有一個最大的特點是:不論是多個線性同一時間內對一個對象進行多次同步還是一個線程對同一個對象同步多次,一個對象只分配一把遞歸互斥鎖。也就意味著對同一個對象而言,當執行某一次同步操作時,其他線程或同一線程的其他同步操作都會被阻塞,不言而喻,這種加鎖方式的效率是很低的。
下面代碼展示了@synchronized經典的低效率使用案例之一:
- (void)setInstanceMemberObjecObject1:(id)value {
@synchronized(self) {
self.instanceMember1 = value;
}
}
- (void)setInstanceMemberObjecObject2:(id)value {
@synchronized(self) {
self.instanceMember2 = value;
}
}
- (void)setInstanceMemberObjecObject3:(id)value {
@synchronized(self) {
self.instanceMember3 = value;
}
}
上述代碼,調用其中一個設置函數時,另外兩個成員變量的設置函數在同一時間被調用都會被阻塞。這里@synchronized同步的代碼很簡單,所以不會效率差別不大。如果是同步的代碼需要執行較長的時間,且被多個線程并發調用,那么效率變得很低。如果不清楚@synchronized的實現原理,可能很難排查出來導致效率低下的問題所在。我建議使用GCD取代@synchronized實現同步功能,GCD不僅是線程安全,且其由底層實現,效率會好很多。我們發生@synchronized的底層實現有捕獲異常的功能,因此適合在需要確保發生錯誤時代碼不會死鎖,而是拋出異常時使用。
博客地址