正確使用多線程同步鎖@synchronized()
在上篇多線程安全的文章中,我曾推薦過大家使用@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<SyncList> sDataLists;
再看下StripedMap的實現就很清楚了:
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對應的一個系統維護的遞歸鎖。
以上就是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解釋清楚:)
來自:http://www.jianshu.com/p/2dc347464188