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í)足夠?。?br> 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 開發(fā)中的鎖
iOS開發(fā)-多線程開發(fā)之線程安全篇
iOS 中幾種常用的鎖總結(jié)
iOS中的5種鎖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容