在上篇多線程安全的文章中,我曾推薦過大家使用@synchronized來使得代碼獲得原子性,從而保證多線程安全。這篇文章向大家介紹一些@synchronized的知識點和應該避免的坑。
@synchronized原理
@synchronized是幾種iOS多線程同步機制中最慢的一個,同時也是最方便的一個相叁。
蘋果建立@synchronized的初衷就是方便開發(fā)者快速的實現(xiàn)代碼同步,語法如下:
@synchronized(obj) {
//code
}
為了加深理解辽幌,我們刨一刨代碼看看@synchronized到底做了什么事增淹。我在一個測試工程的main.m中寫了一段代碼:
void testSync()
{
NSObject* obj = [NSObject new];
@synchronized (obj) {
}
}
然后在Xcode中選擇菜單Product->Perform Action->Assemble "main.m",就得到了如下的匯編代碼:
上圖中我將關鍵代碼用紅線標出了乌企,很容易就定位到了我們的目標代碼虑润。
ARC幫我們插入的retain,release也在其中:)加酵,我們感興趣的部分是下面兩個函數(shù):
bl _objc_sync_enter
bl _objc_sync_exit
這兩個函數(shù)應該就是synchronized進入和退出的調用拳喻,下面去Objective C的源碼里找找 :)
在源碼中一搜,很快就發(fā)現(xiàn)了這兩個函數(shù):
// 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關鍵字亚亲,發(fā)現(xiàn)也是使用的遞歸鎖,看來這是個common trick腐缤。recursive mutex其實里面還是使用了pthread_mutex_t捌归,只不過多了一層ownership的判斷,性能上比非遞歸鎖要稍微慢一些岭粤。
@synchronized(nil)不起任何作用惜索,表明我們需要適當關注傳入的object的聲明周期,一旦置為nil之后就無法做代碼同步了绍在。
我們再看看傳入的obj參數(shù)有什么作用门扇。
繼續(xù)看代碼發(fā)現(xiàn)傳入的obj被用作參數(shù)來獲取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<SyncList> sDataLists;
再看下StripedMap的實現(xiàn)就很清楚了:
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;
}
indexForPointer中使用了obj的內存地址留攒,做了個簡單的map,映射到另一個內存空間來存放SyncList嫉嘀。
通過上述分析炼邀,我們可以得出結論了:
synchronized中傳入的object的內存地址,被用作key剪侮,通過hash map對應的一個系統(tǒng)維護的遞歸鎖拭宁。
以上就是object的用處,所以不管是傳入什么類型的object瓣俯,只要是有內存地址杰标,就能啟動同步代碼塊的效果。
消化完synchronized的內部實現(xiàn)彩匕,我們再來看看平常使用中常見的一些坑腔剂。
慎用@synchronized(self)
我其實更想說:不要使用@synchronized(self)。
我看過不少代碼都是直接將self傳入@synchronized當中驼仪,這是種很粗糙的使用方式掸犬,容易導致死鎖的出現(xiàn)。比如:
//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)
。兩個公共鎖交替使用的場景就容易出現(xiàn)死鎖毡泻。
所以正確的做法是傳入一個類內部維護的NSObject對象胜茧,而且這個對象是對外不可見的。
精準的粒度控制
有些人說@synchronized慢仇味,但@synchronized和其他同步鎖的性能相比并沒有很夸張呻顽,對于使用者來說幾乎忽略不計。
之所以慢是更多的因為沒有做好粒度控制丹墨。鎖本質上是為了讓我們的一段代碼獲得原子性廊遍,不同的critical section要使用不同的鎖。我見過很多類似的寫法:
@synchronized (sharedToken) {
[arrA addObject:obj];
}
@synchronized (sharedToken) {
[arrB addObject:obj];
}
使用同一個token來同步arrA和arrB的訪問贩挣,雖然arrA和arrB之間沒有任何聯(lián)系喉前。傳入self的就更不對了没酣。
應該是不同的數(shù)據(jù)使用不同的鎖,盡量將粒度控制在最細的程度卵迂。上述代碼應該是:
@synchronized (tokenA) {
[arrA addObject:obj];
}
@synchronized (tokenB) {
[arrB addObject:obj];
}
注意內部的函數(shù)調用
@synchronized還有個很容易變慢的場景裕便,就是{}內部有其他隱蔽的函數(shù)調用。比如:
@synchronized (tokenA) {
[arrA addObject:obj];
[self doSomethingWithA:arrA];
}
doSomethingWithA內部可能又調用了其他函數(shù)见咒,維護doSomethingWithA的工程師可能并沒有意識到自己是被鎖同步的偿衰,由此層層疊疊可能引入更多的函數(shù)調用,代碼就莫名其妙的越來越慢了改览,感覺鎖的性能差下翎,其實是我們沒用好。
所以在書寫@synchronized內部代碼的時候宝当,要十分小心內部隱蔽的函數(shù)調用视事。
總結
看似簡單的API調用,背后其實包含了不少知識庆揩,知其所以然才能運用得當郑口。關于@synchronized(xxx)就介紹到這里,希望有將synchronized解釋清楚:)