本文首發於 我的博客git
在IOS開發中,同步鎖相信你們都使用過,即 @synchronized
,這篇文章向你們介紹一些 @synchronized
的原理和使用。github
@synchronized
是IOS多線程同步中性能最差的:數組
倒是使用起來最方便的一個,一般咱們這麼用:sass
@synchronized (self) {
// code
}
複製代碼
爲了瞭解其底層是如何 實現的,咱們在測試工程的ViewController.m中寫了一段代碼安全
static NSString *token = @"synchronized-token";
- (void)viewDidLoad {
[super viewDidLoad];
@synchronized (token) {
// code
NSLog(@"haha");
}
}
複製代碼
點擊Xcode
--> Debug
-->Debug Workflow
--> Always Show Disassembly
顯示彙編,打上斷點啓動真機,就看到了以下代碼:markdown
上圖中的全部方法的調用我都圈出來了,ARC
幫咱們自動插入了retain
和release
,並且還找到了NSLog的方法,那麼包含NSLog的正是 objc_sync_enter
和 objc_sync_exit
咱們猜想這個應該就是 @synchronized
的具體實現,因此咱們來到 Objective-c源碼 處查找,很快咱們發現了這兩個函數的實現:多線程
// 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;
}
複製代碼
從官方代碼和註釋中咱們能夠得出:async
@synchronized
會建立一個遞歸(recursive)
互斥(mutex)
的鎖與 obj
參數進行關聯。@synchronized(nil)
does nothing遞歸鎖其實就是爲了方便同步鎖的嵌套使用而不會出現死鎖的狀況:函數
static NSString *token = @"synchronized-token";
static NSString *token1 = @"synchronized-token1";
- (void)viewDidLoad {
[super viewDidLoad];
@synchronized (token) {
NSLog(@"haha");
@synchronized (token1) {
NSLog(@"didi");
}
}
}
複製代碼
@synchronized(nil) 不起任何做用,說明咱們要注意傳入obj
的生命週期,由於當obj被釋放這個地方就起不到加鎖的做用,有同窗可能注意到我這爲何沒有用self 做爲obj傳遞參數,就是爲了不token
被多個地方持有修改,一旦出現nil
,可能就會出現線程安全問題,這塊我會在後面去驗證。oop
咱們的@synchronized
是針對傳入參數obj
作綁定的,那麼內部obj
到底是幹嗎用的,並且咱們知道@synchronized
在object-c
全局均可以使用,那麼@synchronized
是如何區分不一樣的obj進行一一對應的,帶着這些問題,咱們看看底層對obj到底是如何處理的。
首先咱們看到底層是這樣處理傳入的對象 obj 的:
SyncData* data = id2data(obj, ACQUIRE);
複製代碼
內部都會將 obj
轉化成相應的 SyncData
類型的對象,而後 id2data
內部是下面這樣取的:
SyncData **listp = &LIST_FOR_OBJ(object);
複製代碼
看看LIST_FOR_OBJ
是如何操做obj
的(下面代碼留下關鍵部分,具體細節請自行前往源碼查看):
1// obj傳入sDataLists
2#define LIST_FOR_OBJ(obj) sDataLists[obj].data
3
4// 哈希表結構,內部存SyncList
5static StripedMap<SyncList> sDataLists;
6
7// SyncList結構體,內部data就是SyncData
8struct SyncList {
9 SyncData *data;
10 spinlock_t lock;
11 constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
12};
13
14// 哈希表結構
15class StripedMap {
16 enum { StripeCount = 64 };
17
18 struct PaddedT {
19 T value alignas(CacheLineSize);
20 };
21
22 PaddedT array[StripeCount];
23
24 // 哈希函數
25 static unsigned int indexForPointer(const void *p) {
26 uintptr_t addr = reinterpret_cast<uintptr_t>(p);
27 return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
28
29 }
30
31 public:
32 // 此處的p就是上面的obj,也就是obj執行上面的哈希函數對應到數組的index
33 T& operator[] (const void *p) {
34 return array[indexForPointer(p)].value;
35 }
複製代碼
從上述代碼看出總體StripedMap
是一個哈希表結構,表外層是一個數組,數組裏的每一個位置存儲一個相似鏈表的結構(SyncList
),SyncData
存儲的位置具體依賴第25
行處的哈希函數,如圖:
obj1
處,通過哈希函數計算得出索引2
,起初咱們要順着上面的 A
線對List
進行查找,沒找到,將當前的obj
插入到最前面,也是爲了更快的找到當前使用的對象而這麼設計。
// 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;
複製代碼
obj2
處就很少分析了,找到直接返回,進行加鎖解鎖處理。
@synchronized
中傳入object
的內存地址,被用做key
,經過必定的hash
函數映射到一個系統全局維護的遞歸鎖中,因此不論傳入什麼類型的值,只要它有內存地址就能夠達到同步鎖的效果。
引用這篇文章開頭所說的例子:
一般咱們直接用@synchronized(self)
沒毛病,可是很粗糙,也確實存在問題。是否是他喵的被我說亂了,到底有沒有問題?
@property (nonatomic, strong) NSMutableArray *array;
for (NSInteger i = 0; i < 20000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self->_array = [NSMutableArray array];
});
}
複製代碼
這塊代碼咱們知道是有問題的,會直接Crash
,緣由就在於多線程同時操做array,致使在某一個瞬間可能同時釋放了屢次,也就是野指針的問題。那麼咱們嘗試用今天的 @synchronized
來同步鎖一下:
for (NSInteger i = 0; i < 20000; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self->_array) {
self->_array = [NSMutableArray array];
}
});
}
複製代碼
調試一下發現依然崩潰,@synchronized
不是加鎖嗎,怎麼還會Crash
呢?
原來咱們綁定的對象是array,而內部多線程對array的操做倒是頻繁的建立和release,當某個瞬間arry執行了release的時候就達成了咱們所說的 @synchronized(nil)
,上文已經分析了這個時候do nothing !
因此能起到同步鎖的做用麼,很顯然不能,這就是崩潰的主要緣由。
因而可知@synchronized
在一些不斷的循環,遞歸的時候並不如人意,咱們惟獨要注意obj的參數惟一化,也就是與所要鎖的對象一一對應,這樣就避免了多個地方持有obj。
static NSString *token = @"synchronized-token";
@synchronized (token) {
self.array1 = [NSMutableArray array];
}
--------------------------------------------------
static NSString *another_token = @"another_token";
@synchronized (another_token) {
self.array2 = [NSMutableArray array];
}
複製代碼
雖然 @synchronized
使用起來很簡單,可是其內部的實現倒是很複雜,儘管其性能不好,適當的地方使用也無可厚非,無非就是注意對象的生命週期,以及內部的嵌套等。但願這篇文章能把@synchronized
講清楚,歡迎指正和溝通。