正確使用多線程同步鎖@synchronized()

在上篇多線程安全的文章中,我曾推薦過大家使用@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解釋清楚:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末盾鳞,一起剝皮案震驚了整個濱河市犬性,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腾仅,老刑警劉巖乒裆,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異推励,居然都是意外死亡鹤耍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門验辞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稿黄,“玉大人,你說我怎么就攤上這事跌造「伺拢” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵壳贪,是天一觀的道長陵珍。 經常有香客問我,道長违施,這世上最難降的妖魔是什么互纯? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮磕蒲,結果婚禮上留潦,老公的妹妹穿的比我還像新娘只盹。我一直安慰自己,他們只是感情好兔院,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布殖卑。 她就那樣靜靜地躺著,像睡著了一般秆乳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钻哩,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天屹堰,我揣著相機與錄音,去河邊找鬼街氢。 笑死扯键,一個胖子當著我的面吹牛,可吹牛的內容都是我干的珊肃。 我是一名探鬼主播荣刑,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伦乔!你這毒婦竟也來了厉亏?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤烈和,失蹤者是張志新(化名)和其女友劉穎爱只,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體招刹,經...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡恬试,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了疯暑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片训柴。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖妇拯,靈堂內的尸體忽然破棺而出幻馁,到底是詐尸還是另有隱情,我是刑警寧澤越锈,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布宣赔,位于F島的核電站,受9級特大地震影響瞪浸,放射性物質發(fā)生泄漏儒将。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一对蒲、第九天 我趴在偏房一處隱蔽的房頂上張望钩蚊。 院中可真熱鬧贡翘,春花似錦、人聲如沸砰逻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蝠咆。三九已至踊东,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間刚操,已是汗流浹背闸翅。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留菊霜,地道東北人坚冀。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像鉴逞,于是被迫代替她去往敵國和親记某。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內容