在上篇多線程安全的文章中,我曾推薦過大家使用@synchronized來使得代碼獲得原子性,從而保證多線程安全。這篇文章向大家介紹一些@synchronized的知識點和應該避免的坑。
@synchronized原理
@synchronized是幾種iOS多線程同步機制中最慢的一個,同時也是最方便的一個。
蘋果建立@synchronized的初衷就是方便開發者快速的實現代碼同步,語法如下:
@synchronized(obj) { //code }
為了加深理解,我們刨一刨代碼看看@synchronized到底做了什麼事。我在一個測試工程的main.m中寫了一段代碼:
void testSync() { NSObject* obj = [NSObject new]; @synchronized (obj) { } }
然後在Xcode中選擇菜單Product->Perform Action->Assemble “main.m”,就得到了如下的匯編代碼:
上圖中我將關鍵代碼用紅線標出了,很容易就定位到了我們的目標代碼。
ARC幫我們插入的retain,release也在其中:),我們感興趣的部分是下面兩個函數:
bl _objc_sync_enter bl _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; }
從上述源碼中,我們至少可以確立兩個信息:
synchronized是使用的遞歸mutex來做同步。
@synchronized(nil)不起任何作用
遞歸mutex的意思是,我們可以寫如下代碼:
@synchronized (obj) { NSLog(@"1st sync"); @synchronized (obj) { NSLog(@"2nd sync"); } }
而不會導致死鎖。我順道扒了下java當中的synchronized關鍵字,發現也是使用的遞歸鎖,看來這是個common trick。recursive mutex其實裡面還是使用了pthread_mutex_t,只不過多了一層ownership的判斷,性能上比非遞歸鎖要稍微慢一些。
@synchronized(nil)不起任何作用,表明我們需要適當關注傳入的object的聲明周期,一旦置為nil之後就無法做代碼同步了。
我們再看看傳入的obj參數有什麼作用。
繼續看代碼發現傳入的obj被用作參數來獲取SyncData對象,裡面有一大段關於SyncData的cache邏輯,有興趣的同學可以自己看下代碼,這是一個兩層的cache設計,第一層是tls cache,第二層是自己維護的一個hash map。這裡將流程簡化,來看下obj是如何在hash map中緩存的。
先看下SyncData獲取的方式:
SyncData **listp = &LIST_FOR_OBJ(object);
而LIST_FOR_OBJ又指向:
#define LIST_FOR_OBJ(obj) sDataLists[obj].data static StripedMap sDataLists;
再看下StripedMap的實現就很清楚了:
static unsigned int indexForPointer(const void *p) { uintptr_t addr = reinterpret_cast(p); return ((addr >> 4) ^ (addr >> 9)) % StripeCount; } public: T& operator[] (const void *p) { return array[indexForPointer(p)].value; }
indexForPointer中使用了obj的內存地址,做了個簡單的map,映射到另一個內存空間來存放SyncList。
通過上述分析,我們可以得出結論了:
synchronized中傳入的object的內存地址,被用作key,通過hash map對應的一個系統維護的遞歸鎖。
以上就是object的用處,所以不管是傳入什麼類型的object,只要是有內存地址,就能啟動同步代碼塊的效果。
消化完synchronized的內部實現,我們再來看看平常使用中常見的一些坑。
慎用@synchronized(self)
我其實更想說:不要使用@synchronized(self)。
我看過不少代碼都是直接將self傳入@synchronized當中,這是種很粗糙的使用方式,容易導致死鎖的出現。比如:
//class A @synchronized (self) { [_sharedLock lock]; NSLog(@"code in class A"); [_sharedLock unlock]; } //class B [_sharedLock lock]; @synchronized (objectA) { NSLog(@"code in class B"); } [_sharedLock unlock];
原因是因為self很可能會被外部對象訪問,被用作key來生成一鎖,類似上述代碼中的@synchronized (objectA)。兩個公共鎖交替使用的場景就容易出現死鎖。
所以正確的做法是傳入一個類內部維護的NSObject對象,而且這個對象是對外不可見的。
精准的粒度控制
有些人說@synchronized慢,但@synchronized和其他同步鎖的性能相比並沒有很誇張,對於使用者來說幾乎忽略不計。
之所以慢是更多的因為沒有做好粒度控制。鎖本質上是為了讓我們的一段代碼獲得原子性,不同的critical section要使用不同的鎖。我見過很多類似的寫法:
@synchronized (sharedToken) { [arrA addObject:obj]; } @synchronized (sharedToken) { [arrB addObject:obj]; }
使用同一個token來同步arrA和arrB的訪問,雖然arrA和arrB之間沒有任何聯系。傳入self的就更不對了。
應該是不同的數據使用不同的鎖,盡量將粒度控制在最細的程度。上述代碼應該是:
@synchronized (tokenA) { [arrA addObject:obj]; } @synchronized (tokenB) { [arrB addObject:obj]; }
注意內部的函數調用
@synchronized還有個很容易變慢的場景,就是{}內部有其他隱蔽的函數調用。比如:
@synchronized (tokenA) { [arrA addObject:obj]; [self doSomethingWithA:arrA]; }
doSomethingWithA內部可能又調用了其他函數,維護doSomethingWithA的工程師可能並沒有意識到自己是被鎖同步的,由此層層疊疊可能引入更多的函數調用,代碼就莫名其妙的越來越慢了,感覺鎖的性能差,其實是我們沒用好。
所以在書寫@synchronized內部代碼的時候,要十分小心內部隱蔽的函數調用。
總結
看似簡單的API調用,背後其實包含了不少知識,知其所以然才能運用得當。關於@synchronized(xxx)就介紹到這裡,希望有將synchronized解釋清楚:)