OC 多線程基礎(chǔ)知識:鎖

想要深入理解多線程蚀狰,鎖是預(yù)備知識,這里總結(jié)一下OC中鎖相關(guān)的知識职员,打好基礎(chǔ)麻蹋。

為什么要有鎖?

鎖概念的提出焊切,是為了解決多線程資源共享的問題扮授,在多線程環(huán)境下,有的資源可能會同時被多個線程訪問专肪,可能會出現(xiàn)資源搶奪的問題刹勃。這里引入一個概念叫臨界區(qū)(Critical Section),就是一段代碼嚎尤,同一時間只能由一個線程訪問荔仁,以保障臨界區(qū)內(nèi)的線程是安全的(資源不被搶奪,改變)芽死。鎖就是用來解決臨界區(qū)內(nèi)線程安全的問題乏梁。

下面介紹三種常見的鎖:

自旋鎖(spin lock)

自旋鎖長這樣:

while (搶鎖(lock) == 沒搶到) {
}

就是利用一個while循環(huán),不斷的嘗試去搶鎖(這里的鎖lock是一個抽象的概念收奔,可以是一個整數(shù)掌呜,一開始是1,表示沒有鎖坪哄,搶到后變?yōu)?质蕉,表示有鎖),搶到了鎖就跳出循環(huán)翩肌,搶不到鎖就不斷重試模暗。

自旋鎖的缺點(diǎn)很明顯,不斷的搶鎖會占用CPU資源念祭。優(yōu)點(diǎn)是線程不用休眠兑宇,不用花時間在上下文切換(context switch)從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài),用在輕量級的臨界區(qū)上效率高粱坤。

互斥鎖(mutex

互斥鎖長這樣:

while (搶鎖(lock) == 沒搶到) {
    線程休眠隶糕,請?jiān)谶@把鎖的狀態(tài)發(fā)生改變時再喚醒(lock);
}

和自旋鎖很相似瓷产,不同就是搶不到鎖的時候,讓線程去休眠枚驻,當(dāng)鎖的狀態(tài)改變的時候再喚醒該線程濒旦。

互斥鎖的缺點(diǎn)是,線程休眠會讓線程從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài)再登,喚醒的時候從內(nèi)核態(tài)轉(zhuǎn)化為用戶態(tài)尔邓,需要兩次上下文切換,花費(fèi)大量時間锉矢。優(yōu)點(diǎn)是不用忙等梯嗽,臨界區(qū)很長時效率高。

讀寫鎖(readers-writer lock

讀寫鎖就是分了兩種情況沽损,一種是讀時的鎖灯节,一種是寫時的鎖,同時規(guī)定:

  • 同時可以存在多個讀鎖缠俺,也就是讀-讀不互斥
  • 只能存在一個寫鎖显晶,也就是讀-寫互斥贷岸,寫-寫互斥

讀寫鎖的實(shí)現(xiàn)時用了兩個互斥鎖(或者兩個自旋鎖):

//讀者加鎖
- (void)readerLock {
    加鎖(rlock);
    condition++;
    if (condition == 1) {
        加鎖(wlock);
    }
    解鎖(rlock);
}
//讀者解鎖
- (void)readerUnlock {
    加鎖(rlock);
    condition--;
    if (condition == 0) {
        加鎖(wlock);
    }
    解鎖(rlock);
}
//寫者加鎖
- (void)writerLock {
    加鎖(wlock);
}
//寫者解鎖
- (void)writerUnlock {
    解鎖(wlock);
}
@end

這里我們用了兩把互斥鎖(rlock壹士,wlock)來實(shí)現(xiàn)讀寫鎖,利用了:

  • 計(jì)數(shù)器condition跟蹤被阻塞的讀線程偿警。如果先有寫鎖躏救,讀鎖中condition==1,讀會被wlock阻塞螟蒸。如果先有讀鎖盒使,寫鎖會被wlock堵塞;讀鎖再次獲取時七嫌,可以使condition>1少办,從而讀鎖不被堵塞。
  • 互斥鎖rlock保護(hù)condition诵原,供讀者使用
  • 互斥鎖wlock 確保寫操作互斥

下面介紹一個更高級的實(shí)現(xiàn)讀寫鎖的方法:條件變量+互斥鎖

首先介紹一下條件變量

條件變量可以簡單理解為英妓,一個條件,如果達(dá)成了就發(fā)通知绍赛。這樣說有點(diǎn)抽象蔓纠,把條件變量用到讀寫鎖里就清楚了:

//讀者加鎖
- (void)readerLock {
    加鎖(rwlock);
    while (self.isWriting) {
        解鎖,等待條件變量達(dá)成時的通知喚醒吗蚌,再加鎖(cond, rwlock);
    }
    self.readCount++;
    解鎖(rwlock);
}
//讀者解鎖
- (void)readerUnlock {
   加鎖(rwlock);
   self.readCount--;
   if (self.readCount == 0) {
       //喚起一條寫的線程
       條件變量達(dá)成時腿倚,觸發(fā)通知(cond);
   }
   解鎖(rwlock);
}
//寫者加鎖
- (void)writerLock {
    加鎖(rwlock);
    while (self.isWriting || self.readCount > 0) {
         解鎖,等待條件變量達(dá)成時的通知喚醒蚯妇,再加鎖(cond, rwlock);
    }
    self.isWriting = YES;
    解鎖(rwlock);
}
//寫者解鎖
- (void)writerUnlock {
    加鎖(rwlock);
    self.isWriting = NO;
    //喚起多個讀的線程
    條件變量達(dá)成時的敷燎,觸發(fā)通知(cond);
    解鎖(rwlock);
}
@end

這里使用了使用[條件變量cond與普通的互斥鎖rwlock暂筝、整型計(jì)數(shù)器readCount(表示正在讀的個數(shù))與布爾標(biāo)志isWrite(表示正在寫)來實(shí)現(xiàn)讀寫鎖。

  • 當(dāng)有讀鎖時硬贯,readCount>0乖杠,寫鎖進(jìn)入while循環(huán),只有條件變量達(dá)成時澄成,才會收到通知喚醒跳出循環(huán)胧洒,條件變量達(dá)成的條件就是讀鎖全部釋放(readCount==0)。
  • 當(dāng)有寫鎖時墨状,isWriting == YES卫漫,讀鎖進(jìn)入while循環(huán),只有條件變量達(dá)成時肾砂,才會收到通知喚醒出循環(huán)列赎,條件變量達(dá)成的條件就是寫鎖釋放(isWriting == NO)。

下面總結(jié)一個OC鐘常用鎖的用法镐确,一下所有例子包吝,都用賣股票的例子:

@synchronized 關(guān)鍵字

@synchronized(這里添加一個OC對象,一般使用self) {
    要加鎖的代碼
}

這是一個互斥鎖源葫,簡單易用诗越,但性能最差,建議加鎖的代碼盡量少息堂,例子如下:

- (void)viewDidLoad {
    [super viewDidLoad];

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

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

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

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

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        @synchronized(self) {
            if (self.tickets > 0) {
                self.tickets -= 1;
                NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"票賣完了  Thread:%@", [NSThread currentThread]);
                break;
            }
        }
    }
}

// 剩余票數(shù)=4, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 票賣完了  Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 票賣完了  Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}

NSLock

_mutexLock = [[NSLock alloc] init];
[_mutexLock lock];
[_mutexLock unlock];

互斥鎖嚷狞,有lockunlock方法:

- (void)viewDidLoad {
    [super viewDidLoad];

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

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

    _mutexLock = [[NSLock alloc] init];

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

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

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        [_mutexLock lock];
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票賣完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        [_mutexLock unlock];
    }
}

// 剩余票數(shù)=4, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 票賣完了  Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}

pthread_mutex

pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);

互斥鎖

- (void)viewDidLoad {
    [super viewDidLoad];

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

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // @property pthread_mutex_t mutex;
    pthread_mutex_init(&_mutex, NULL);

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

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

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        pthread_mutex_lock(&_mutex);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票賣完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        pthread_mutex_unlock(&_mutex);
    }
}

@end
  
// 剩余票數(shù)=4, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 票賣完了  Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}

dispatch_semaphore

_semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_semaphore);

信號量實(shí)現(xiàn)加鎖,線程獲取一個信號量時荣堰,信號量數(shù)量減一床未,線程釋放信號量時,信號量數(shù)量加一振坚,信號量數(shù)量大于等于零時薇搁,加鎖的代碼可以執(zhí)行《砂耍互斥鎖可以看做是一種特殊的信號量(初始信號量等于一)啃洋。

- (void)viewDidLoad {
    [super viewDidLoad];

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

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    _semaphore = dispatch_semaphore_create(1);

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

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

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票賣完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        dispatch_semaphore_signal(_semaphore);
    }
}

@end
  
// 剩余票數(shù)=4, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 票賣完了  Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}

OSSpinLock

_pinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_pinLock);
OSSpinLockUnlock(&_pinLock);

自旋鎖,效率最高呀狼,但有隱患:

可能會出現(xiàn)優(yōu)先級翻轉(zhuǎn)的情況裂允。比如線程1優(yōu)先級比較高,線程2優(yōu)先級比較低哥艇,然后在某一時刻是線程2先獲取到鎖绝编,所以先是線程2加鎖,這時候,線程1就在while(目標(biāo)鎖還未釋放)十饥,這個狀態(tài)窟勃,但因?yàn)榫€程1優(yōu)先級比較高,所以系統(tǒng)分配的時間比較多逗堵,有可能會沒有分配時間給線程2執(zhí)行后續(xù)的操作(需要做的任務(wù)和解鎖)了秉氧,這時候就會造成死鎖。

所以iOS10之后自旋鎖OSSpinLock就被os_unfair_lock(底層是互斥鎖)替換了蜒秤。

- (void)viewDidLoad {
    [super viewDidLoad];

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

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // #import <libkern/OSAtomic.h>
    // @property OSSpinLock pinLock;
    // 'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
    _pinLock = OS_SPINLOCK_INIT;

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

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

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        OSSpinLockLock(&_pinLock);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票數(shù)=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票賣完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        OSSpinLockUnlock(&_pinLock);
    }
}

@end
  
// 剩余票數(shù)=4, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票數(shù)=3, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票數(shù)=2, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票數(shù)=1, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票數(shù)=0, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 票賣完了  Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末汁咏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子作媚,更是在濱河造成了極大的恐慌攘滩,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纸泡,死亡現(xiàn)場離奇詭異漂问,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)女揭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門蚤假,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吧兔,你說我怎么就攤上這事磷仰。” “怎么了掩驱?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵芒划,是天一觀的道長。 經(jīng)常有香客問我欧穴,道長,這世上最難降的妖魔是什么泵殴? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任涮帘,我火速辦了婚禮,結(jié)果婚禮上笑诅,老公的妹妹穿的比我還像新娘调缨。我一直安慰自己,他們只是感情好吆你,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布弦叶。 她就那樣靜靜地躺著,像睡著了一般妇多。 火紅的嫁衣襯著肌膚如雪伤哺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機(jī)與錄音立莉,去河邊找鬼绢彤。 笑死,一個胖子當(dāng)著我的面吹牛蜓耻,可吹牛的內(nèi)容都是我干的茫舶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼刹淌,長吁一口氣:“原來是場噩夢啊……” “哼饶氏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起有勾,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嚷往,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后柠衅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皮仁,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年菲宴,在試婚紗的時候發(fā)現(xiàn)自己被綠了贷祈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡喝峦,死狀恐怖势誊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谣蠢,我是刑警寧澤粟耻,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站眉踱,受9級特大地震影響挤忙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谈喳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一册烈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婿禽,春花似錦赏僧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膛壹,卻和暖如春驾中,著一層夾襖步出監(jiān)牢的瞬間唉堪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工哀卫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巨坊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓此改,卻偏偏與公主長得像趾撵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子共啃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359

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