iOS-多線程-鎖

多線程需要一種互斥的機(jī)制來(lái)訪問(wèn)共享資源。

一浇冰、 互斥鎖

互斥鎖的意思是某一時(shí)刻只允許一個(gè)線程訪問(wèn)某一資源曾掂。為了保證這一點(diǎn),每個(gè)想要訪問(wèn)共享資源的線程顽腾,需要首先獲得一個(gè)共享資源的互斥鎖近零,一旦某個(gè)線程對(duì)共享資源完成了訪問(wèn),就釋放掉這個(gè)互斥鎖,這樣別的線程就有機(jī)會(huì)獲取互斥鎖久信,然后訪問(wèn)該共享資源了猪瞬。

一般情況下,一個(gè)線程只能申請(qǐng)一次鎖入篮,也只能在獲得鎖的情況下才能釋放鎖,多次申請(qǐng)鎖釋放未獲得的鎖都會(huì)導(dǎo)致崩潰幌甘。假設(shè)在已經(jīng)獲得鎖的情況下再次申請(qǐng)鎖潮售,線程會(huì)因?yàn)榈却i的釋放而進(jìn)入睡眠狀態(tài),因此就不可能再釋放鎖锅风,從而導(dǎo)致死鎖酥诽。

互斥鎖的實(shí)現(xiàn)原理與信號(hào)量非常相似,不是使用忙等皱埠,而是阻塞線程并睡眠肮帐,需要進(jìn)行上下文切換。

1. pthread_mutex

由于 pthread_mutex 有多種類型边器,可以支持遞歸鎖等训枢,因此在申請(qǐng)加鎖時(shí),需要對(duì)鎖的類型加以判斷忘巧,這也就是為什么它和信號(hào)量的實(shí)現(xiàn)類似恒界,但效率略低的原因。

如果已經(jīng)得到鎖砚嘴,再次申請(qǐng)鎖十酣,會(huì)導(dǎo)致死鎖。然而這種情況經(jīng)常會(huì)發(fā)生际长,比如某個(gè)函數(shù)申請(qǐng)了鎖耸采,在臨界區(qū)內(nèi)又遞歸調(diào)用了自己。辛運(yùn)的是 pthread_mutex 支持遞歸鎖工育,也就是允許一個(gè)線程遞歸的申請(qǐng)鎖虾宇,只要把 attr 的類型改成 PTHREAD_MUTEX_RECURSIVE 即可。


712028-c2d5d99ae4fb9cfc.png

2. NSLock

NSLock只是在內(nèi)部封裝了一個(gè)pthread_mutex翅娶,屬性為PTHREAD_MUTEX_ERRORCHECK文留,它會(huì)損失一定性能換來(lái)錯(cuò)誤提示。這里使用宏定義的原因是竭沫,OC 內(nèi)部還有其他幾種鎖燥翅,他們的 lock 方法都是一模一樣,僅僅是內(nèi)部pthread_mutex
互斥鎖的類型不同蜕提。通過(guò)宏定義森书,可以簡(jiǎn)化方法的定義。
NSLock比pthread_mutex略慢的原因在于它需要經(jīng)過(guò)方法調(diào)用,同時(shí)由于緩存的存在凛膏,多次方法調(diào)用不會(huì)對(duì)性能產(chǎn)生太大的影響杨名。

//設(shè)置票的數(shù)量為5
    _tickets = 5;

    //創(chuàng)建鎖
    _mutexLock = [[NSLock alloc] init];

    //線程1
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });

    //線程2
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });

- (void)saleTickets
{

    while (1) {
        [NSThread sleepForTimeInterval:1];
        //加鎖
        [_mutexLock lock];
        if (_tickets > 0) {
            _tickets--;
            NSLog(@"剩余票數(shù)= %ld, Thread:%@",_tickets,[NSThread currentThread]);        
        } else {
            NSLog(@"票賣完了  Thread:%@",[NSThread currentThread]);
            break;
        }
        //解鎖
        [_mutexLock unlock];
    }
}

性能與使用場(chǎng)景
pthread_mutex是pthread經(jīng)典的基于互斥量機(jī)制的同步鎖,特性猖毫、性能以及穩(wěn)定各方面都已被大量項(xiàng)目所驗(yàn)證台谍,也是比較推薦作為常規(guī)同步鎖首選

二、自旋鎖OSSpinLock

上述文章中已經(jīng)介紹了 OSSpinLock 不再安全吁断,主要原因發(fā)生在低優(yōu)先級(jí)線程拿到鎖時(shí)趁蕊,高優(yōu)先級(jí)線程進(jìn)入忙等(busy-wait)狀態(tài),一直循環(huán)仔役,消耗大量 CPU 時(shí)間掷伙,從而導(dǎo)致低優(yōu)先級(jí)線程拿不到 CPU 時(shí)間,也就無(wú)法完成任務(wù)并釋放鎖又兵。這種問(wèn)題被稱為優(yōu)先級(jí)反轉(zhuǎn)任柜。

原理,通過(guò)一個(gè)全局變量和申請(qǐng)鎖的原子操作沛厨。

然而在多處理器的情況下宙地,能夠被多個(gè)處理器同時(shí)執(zhí)行的操作任然算不上原子操作。因此俄烁,真正的原子操作必須由硬件提供支持绸栅,比如 x86 平臺(tái)上如果在指令前面加上 “LOCK” 前綴,對(duì)應(yīng)的機(jī)器碼在執(zhí)行時(shí)會(huì)把總線鎖住页屠,使得其他 CPU不能再執(zhí)行相同操作粹胯,從而從硬件層面確保了操作的原子性。
這些非常底層的概念無(wú)需完全掌握辰企,我們只要知道上述申請(qǐng)鎖的過(guò)程风纠,可以用一個(gè)原子性操作 test_and_set 來(lái)完成。

實(shí)際使用:

#import <libkern/OSAtomic.h>
@interface ViewController ()
{
    OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.number = 10;
    spinlock = OS_SPINLOCK_INIT;
}
- (IBAction)test:(id)sender {
    for (int i = 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
             [self sellTicket];
        });
    }
}
- (void)sellTicket {
    OSSpinLockLock(&spinlock);
    
    if (self.number > 0) {
        self.number--;
        NSLog(@"%@還剩%ld張票",[NSThread currentThread],self.number);
    }
    
    OSSpinLockUnlock(&spinlock);
}
@end

使用場(chǎng)景
如果臨界區(qū)的執(zhí)行時(shí)間過(guò)長(zhǎng)牢贸,使用自旋鎖不是個(gè)好主意竹观,自旋鎖適合短時(shí)間的操作,加鎖性能最快潜索,但不能使用不同優(yōu)先級(jí)臭增。

三、信號(hào)量

不是使用忙等竹习,而是阻塞線程并睡眠誊抛,需要進(jìn)行上下文切換。

缺點(diǎn)
在時(shí)間較短的操作整陌,沒(méi)有自旋鎖高效拗窃,會(huì)有上下文切換的成本瞎领。
優(yōu)點(diǎn)
效率高。

四随夸、條件鎖

1. NSCondition

NSCondition 其實(shí)是封裝了一個(gè)互斥鎖條件變量九默。NSCondition 的底層是通過(guò)條件變量(condition variable) pthread_cond_t 來(lái)實(shí)現(xiàn)的。條件變量有點(diǎn)像信號(hào)量宾毒,提供了線程阻塞與信號(hào)機(jī)制驼修,因此可以用來(lái)阻塞某個(gè)線程,并等待某個(gè)數(shù)據(jù)就緒诈铛,隨后喚醒線程邪锌。它僅僅是控制了線程的執(zhí)行順序。

互斥鎖提供線程安全癌瘾,條件變量提供線程阻塞與信號(hào)機(jī)制。

它的基本用法和NSLock一樣饵溅,這里說(shuō)一下NSCondition的特殊用法妨退。
NSCondition提供更高級(jí)的用法,方法如下:

- (void)wait; //阻塞當(dāng)前線程 直到等待喚醒 
- (BOOL)waitUntilDate:(NSDate *)limit;  //阻塞當(dāng)前線程到一定時(shí)間 之后自動(dòng)喚醒
- (void)signal; //喚醒一條阻塞線程
- (void)broadcast; //喚醒所有阻塞線程

2. NSConditionLock

借助 NSCondition 來(lái)實(shí)現(xiàn)蜕企,它的本質(zhì)就是一個(gè)生產(chǎn)者-消費(fèi)者模型咬荷。“條件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容轻掩。NSConditionLock 的內(nèi)部持有一個(gè) NSCondition 對(duì)象幸乒,以及 _condition_value 屬性,在初始化時(shí)就會(huì)對(duì)這個(gè)屬性進(jìn)行賦值:

// 簡(jiǎn)化版代碼
- (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}

它的 lockWhenCondition 方法其實(shí)就是消費(fèi)者方法:

- (void) lockWhenCondition: (NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}

對(duì)應(yīng)的 unlockWhenCondition 方法則是生產(chǎn)者唇牧,使用了 broadcast 方法通知了所有的消費(fèi)者:

- (void) unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}

具體使用:

//主線程中
    NSConditionLock *theLock = [[NSConditionLock alloc] init];

    //線程1
    dispatch_async(self.concurrentQueue, ^{
        for (int i=0;i<=3;i++)
        {
            [theLock lock];
            NSLog(@"thread1:%d",i);
            sleep(1);
            [theLock unlockWithCondition:i];
        }
    });

    //線程2
    dispatch_async(self.concurrentQueue, ^{
        [theLock lockWhenCondition:2];
        NSLog(@"thread2");
        [theLock unlock];
    });

五罕扎、遞歸鎖NSRecursiveLock

上文已經(jīng)說(shuō)過(guò),遞歸鎖也是通過(guò) pthread_mutex_lock
函數(shù)來(lái)實(shí)現(xiàn)丐重,在函數(shù)內(nèi)部會(huì)判斷鎖的類型腔召,如果顯示是遞歸鎖,就允許遞歸調(diào)用扮惦,僅僅將一個(gè)計(jì)數(shù)器加一臀蛛,鎖的釋放過(guò)程也是同理。
NSRecursiveLock
與 NSLock
的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t
對(duì)象的類型不同崖蜜,前者的類型為 PTHREAD_MUTEX_RECURSIVE
浊仆。

多次調(diào)用不會(huì)阻塞已獲取該鎖的線程,不會(huì)死鎖豫领。
實(shí)際使用:

// 實(shí)例類person
Person *person = [[Person alloc] init];
// 創(chuàng)建鎖對(duì)象
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

// 創(chuàng)建遞歸方法
static void (^testCode)(int);
testCode = ^(int value) {
    [theLock tryLock];
    if (value > 0)
    {
        [person personA];
        [NSThread sleepForTimeInterval:1];
        testCode(value - 1);
    }
    [theLock unlock];
};

//線程A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    testCode(5);
});

//線程B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [theLock lock];
    [person personB];
    [theLock unlock];
});

六抡柿、@synchronized

這其實(shí)是一個(gè) OC 層面的鎖, 主要是通過(guò)犧牲性能換來(lái)語(yǔ)法上的簡(jiǎn)潔與可讀氏堤。
我們知道 @synchronized 后面需要緊跟一個(gè) OC 對(duì)象沙绝,它實(shí)際上是把這個(gè)對(duì)象當(dāng)做鎖來(lái)使用搏明。你調(diào)用 sychronized 的每個(gè)對(duì)象,Objective-C runtime 都會(huì)為其分配一個(gè)遞歸鎖并存儲(chǔ)在哈希表中闪檬。OC 在底層使用了一個(gè)互斥鎖的數(shù)組(你可以理解為鎖池)星著,通過(guò)對(duì)對(duì)象地址哈希值來(lái)得到對(duì)應(yīng)的互斥鎖。

若是在self對(duì)象上頻繁加鎖粗悯,那么程序可能要等另一段與此無(wú)關(guān)的代碼執(zhí)行完畢虚循,才能繼續(xù)執(zhí)行當(dāng)前代碼,這樣做其實(shí)并沒(méi)有必要样傍。
使用場(chǎng)景:創(chuàng)建單例時(shí)使用横缔。

綜合上述分析與討論,總結(jié)有以下幾點(diǎn)原則:

1衫哥、總的來(lái)看茎刚,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
2撤逢、對(duì)于耗時(shí)較大又易沖突的讀操作膛锭,可以使用讀寫鎖代替pthread_mutex;
3蚊荣、如果確認(rèn)僅有set/get的訪問(wèn)操作初狰,可以選用原子操作屬性;
4互例、對(duì)于性能要求苛刻奢入,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時(shí)足夠邢边丁腥光;
5、條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可糊秆;
6柴我、@synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用;

1.自旋鎖:OSSpinLock 在ios中已經(jīng)不是線程安全的了扩然,如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了艘儒,線程會(huì)以死循環(huán)的方式等待鎖,一旦被訪問(wèn)的資源被解鎖夫偶,則等待資源的線程會(huì)立即執(zhí)行界睁。(效率最高,如果一直等不到鎖會(huì)較占用cpu資源)

2.信號(hào)量:dispatch_semaphore是gcd中通過(guò)信號(hào)量來(lái)實(shí)現(xiàn)共享數(shù)據(jù)的數(shù)據(jù)安全兵拢。(效率第二)

3.互斥鎖:pthread_mutex 翻斟,nslock ,synchronized都是互斥鎖说铃。如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了访惜,線程會(huì)進(jìn)入休眠狀態(tài)等待鎖嘹履。一旦被訪問(wèn)的資源被解鎖,則等待資源的線程會(huì)被喚醒债热。(synchronized效率最低)

4.遞歸鎖:pthread_mutex(recursive)與NSRecursiveLock 砾嫉, 多次調(diào)用不會(huì)阻塞已獲取該鎖的線程。

5.條件鎖:nsconditionlock 滿足一定的條件的加鎖和解鎖窒篱,可以實(shí)現(xiàn)依賴關(guān)系焕刮。nscondition條件鎖,也是通過(guò)信號(hào)來(lái)解鎖墙杯,主要用來(lái)實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式配并。

七、我們平時(shí)使用的:

1. @synchronized高镐,一般用在創(chuàng)建單例的時(shí)候溉旋。
2. atomic修飾屬性的關(guān)鍵字,它不是絕對(duì)安全的嫉髓。
3. 一般使用NSLock即可低滩,但是如果方法會(huì)有遞歸調(diào)用則會(huì)死鎖
,這時(shí)我們使用遞歸鎖:
4. OSSpinLock自旋鎖岩喷,輪詢的方式,用于輕量級(jí)的數(shù)據(jù)操作+1/-1监憎。
5. 信號(hào)量:dispatch_semaphore是gcd中通過(guò)信號(hào)量來(lái)實(shí)現(xiàn)共享數(shù)據(jù)的數(shù)據(jù)安全纱意。(效率第二)

資料:
[iOS]深入理解并發(fā)--鎖
深入理解 iOS 開(kāi)發(fā)中的鎖
iOS開(kāi)發(fā)-多線程開(kāi)發(fā)之線程安全篇
iOS 中幾種常用的鎖總結(jié)
iOS中的5種鎖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鲸阔,隨后出現(xiàn)的幾起案子偷霉,更是在濱河造成了極大的恐慌,老刑警劉巖褐筛,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件类少,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡渔扎,警方通過(guò)查閱死者的電腦和手機(jī)硫狞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晃痴,“玉大人残吩,你說(shuō)我怎么就攤上這事√群耍” “怎么了泣侮?”我有些...
    開(kāi)封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)紧唱。 經(jīng)常有香客問(wèn)我活尊,道長(zhǎng)隶校,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任蛹锰,我火速辦了婚禮深胳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宁仔。我一直安慰自己稠屠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布翎苫。 她就那樣靜靜地躺著权埠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煎谍。 梳的紋絲不亂的頭發(fā)上攘蔽,一...
    開(kāi)封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音呐粘,去河邊找鬼满俗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛作岖,可吹牛的內(nèi)容都是我干的唆垃。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼痘儡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辕万!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沉删,我...
    開(kāi)封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤渐尿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后矾瑰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體砖茸,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年殴穴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凉夯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡采幌,死狀恐怖恍涂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情植榕,我是刑警寧澤再沧,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站尊残,受9級(jí)特大地震影響炒瘸,放射性物質(zhì)發(fā)生泄漏淤堵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一顷扩、第九天 我趴在偏房一處隱蔽的房頂上張望拐邪。 院中可真熱鬧,春花似錦隘截、人聲如沸扎阶。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)东臀。三九已至,卻和暖如春犀农,著一層夾襖步出監(jiān)牢的瞬間惰赋,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工呵哨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赁濒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓孟害,卻偏偏與公主長(zhǎng)得像拒炎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挨务,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • iOS線程安全的鎖與性能對(duì)比 一击你、鎖的基本使用方法 1.1、@synchronized 這是我們最熟悉的枷鎖方式耘子,...
    Jacky_Yang閱讀 2,212評(píng)論 0 17
  • 線程安全是怎么產(chǎn)生的 常見(jiàn)比如線程內(nèi)操作了一個(gè)線程外的非線程安全變量,這個(gè)時(shí)候一定要考慮線程安全和同步球切。 - (v...
    幽城88閱讀 656評(píng)論 0 0
  • 鎖是一種同步機(jī)制谷誓,用于多線程環(huán)境中對(duì)資源訪問(wèn)的限制iOS中常見(jiàn)鎖的性能對(duì)比圖(摘自:ibireme): iOS鎖的...
    LiLS閱讀 1,510評(píng)論 0 6
  • demo下載 建議一邊看文章,一邊看代碼吨凑。 聲明:關(guān)于性能的分析是基于我的測(cè)試代碼來(lái)的捍歪,我也看到和網(wǎng)上很多測(cè)試結(jié)果...
    炸街程序猿閱讀 789評(píng)論 0 2
  • 前言 iOS開(kāi)發(fā)中由于各種第三方庫(kù)的高度封裝,對(duì)鎖的使用很少鸵钝,剛好之前面試中被問(wèn)到的關(guān)于并發(fā)編程鎖的問(wèn)題糙臼,都是一知...
    喵渣渣閱讀 3,690評(píng)論 0 33