iOS 底層 day20 多線程 隊列組 安全隱患 鎖本質(zhì) 自旋鎖

一藐窄、多線程和 RunLoop 的碰撞

1. 請問下面的代碼,點擊頁面后打印順序是什么?
代碼
  • 打印 1 2 3
2. 請問下面的代碼氓鄙,點擊頁面后打印順序是什么?
代碼
  • 打印 1 3
  • 為什么 2 沒有被打印呢业舍?請繼續(xù)往下閱讀抖拦,尋找答案
3. -performSelector:withObject 的本質(zhì)
objc 源碼
  • 從蘋果開源的 objc 源碼中升酣,我們發(fā)現(xiàn) -performSelector:withObject 的本質(zhì)其實就是消息發(fā)送。
  • 所以 題目中的 [self performSelector:@selector(test) withObject:nil] 等價于 [self test]
  • 因此态罪,打印 1 2 3 就得到了解釋
4. GNUstep 是什么噩茄?
  • GNUstep 是 GNU 計劃的項目之一,它將 Cocoa 的 OC 庫重新開源實現(xiàn)了一遍
  • 源碼地址:http://gnustep.org/resources/downloads.php
  • 雖然 GNUstep 不是蘋果官方源碼复颈,但還是具有一定的參考價值
5. -performSelector:withObject:afterDelay: 的本質(zhì)
  • 我們從 Xcode 中進(jìn)入 -performSelector:withObject:afterDelay: 的方法說明绩聘,發(fā)現(xiàn)它屬于 NSRunLoop.h 文件下,而蘋果對于這部分代碼是不開源的耗啦。

  • 我們從 GNUstep 可以找到其實現(xiàn)凿菩,大致如下:

`-performSelector:withObject:afterDelay:` 的本質(zhì)
  • 因此可以解答我們上述問題 2 的疑惑,隊列 queue 中沒有啟動 RunLoop帜讲,所以定時器不會生效衅谷。

  • 我們可以嘗試添加 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 運行,就會打印 1 2 3 了(順序一定是 1 2 3舒帮,想不通的可以嘗試思考下喲)会喝。

  • 同時,我們思維拓展一下玩郊,在子線程調(diào)用 [NSTimer scheduledTimerWithTimeInterval:0 repeats:NO block:^(NSTimer * _Nonnull timer) { NSLog(@"2"); }]; 如果沒有啟動子線程的 RunLoop 也是會無效的喲

6. 鞏固練習(xí)肢执,下面代碼會發(fā)生什么?
題目
  • 會打印 1 译红,然后程序在 perform 語句 發(fā)生崩潰预茄,因為沒有 RunLoop 的支持,thread 很快就退出銷毀了侦厚。

二耻陕、GCD 隊列組

1. 代碼實現(xiàn):任務(wù)1 和任務(wù)2 異步并發(fā)執(zhí)行,等任務(wù)1 和任務(wù)2 執(zhí)行完畢刨沦,回到主線程執(zhí)行 任務(wù)3
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)1 %@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)2 %@", [NSThread currentThread]);
        }
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)3 %@", [NSThread currentThread]);
        }
    });
}
2. 代碼實現(xiàn):任務(wù)1 和任務(wù)2 異步并發(fā)執(zhí)行诗宣,等任務(wù)1 和任務(wù)2 執(zhí)行完畢,并發(fā)執(zhí)行 任務(wù)3 和任務(wù)4
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)1 %@", [NSThread currentThread]);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)2 %@", [NSThread currentThread]);
        }
    });
    dispatch_group_notify(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)3 %@", [NSThread currentThread]);
        }
    });
    dispatch_group_notify(group, queue, ^{
        for (int i = 0 ; i < 5; i++) {
            NSLog(@"任務(wù)4 %@", [NSThread currentThread]);
        }
    });
}

三想诅、多線程安全隱患

1. 多線程安全隱患經(jīng)典例子之一:存取取錢
存取取錢問題展示
2. 多線程安全隱患經(jīng)典例子之二:賣票
賣票
3. 多線程安全隱患的解決方案
  • 解決方案:使用線程同步技術(shù)(同步召庞,就是協(xié)同步調(diào),按照預(yù)定先后次序進(jìn)行)
  • 常見的線程同步技術(shù)是:加鎖
加鎖

四来破、多線程同步與鎖的本質(zhì)

1. 為什么需要鎖篮灼?
  • 當(dāng)多個線程共享同一塊內(nèi)存區(qū)域時,我們需要保證任何一個線程在訪問這塊內(nèi)存時徘禁,所看到的內(nèi)容是穩(wěn)定的诅诱。如果所有的線程對這塊內(nèi)存的訪問都只是讀取,那么我們就不需要采取額外措施送朱。
  • 但哪怕其中有一個線程會修改這塊內(nèi)存娘荡,那我們就要對這個線程進(jìn)行同步干旁。之所以要這么做是因為修改內(nèi)存的操作往往都不是原子操作,而是分成多個時鐘周期炮沐,一個線程對內(nèi)存的操作可能在任何一個周期被另一個線程打斷疤孕,從而導(dǎo)致這塊內(nèi)存的內(nèi)容不穩(wěn)定。
  • 所謂線程同步央拖,也就是說當(dāng)好幾個線程對這一塊內(nèi)存操作完畢了祭阀,下一個線程才能接著對這個內(nèi)存進(jìn)行操作。
  • 如何實現(xiàn)線程同步呢鲜戒?這就是加鎖专控。
2. 對誰加鎖?
  • 雖然我們都用過鎖遏餐,但有沒有考慮過這個鎖本質(zhì)上是給誰加呢伦腐?顯然,是對這塊內(nèi)存加鎖失都,好讓某個線程操作這塊內(nèi)存時柏蘑,其他線程進(jìn)不來。
3. 鎖的本質(zhì)(這塊感覺還需要很多更底層知識鋪墊)粹庞?
  • 共享內(nèi)存 A(大小不定)咳焚,另取一塊內(nèi)存 L(占據(jù)最小單元內(nèi)存)來標(biāo)記能否操作 A
  • 流程:線程操作 A 前,要先取出 L 的值庞溜,如果是 1 的話就將其改成 0革半,然后操作 A,訪問完A 就將 L 改成 1流码;如果 L 是 0 的話又官,就說明別的線程正在操作 A,那就需要等到別人釋放漫试。
  • 然后借助原子訪存指令來保證 內(nèi)存 L 的線程安全
  • 所以鎖的本質(zhì):實際上就是對一塊內(nèi)存進(jìn)行原子訪問六敬,而實現(xiàn)鎖的這塊內(nèi)存,他的名字就做叫 - 互斥量

五驾荣、自旋鎖 NSSpinLock

1. 鎖讓線程阻塞有兩種方式
  • ① 讓線程執(zhí)行一個 while 循環(huán)外构,不斷詢問是否輪到自己執(zhí)行代碼了?(自旋鎖)
  • ② 讓線程睡覺秘车,等輪到它執(zhí)行時典勇,將它喚醒劫哼。(互斥鎖)
2. 自旋鎖 NSSpinLock 簡介
  • OSSpinLock 叫做 自旋鎖叮趴,等待鎖的線程會處于忙等(busy-wait)狀態(tài),一直占用著 CPU 資源
  • 目前已經(jīng)不再安全权烧,可能會出現(xiàn)優(yōu)先級反轉(zhuǎn)的問題(iOS10 后已經(jīng)不推薦用)
  • 如果等待鎖的線程優(yōu)先級較高眯亦,它會一直占用著 CPU 資源伤溉,優(yōu)先級低的線程就無法釋放鎖
  • 但是因為它簡單,是我們很好的學(xué)習(xí)對象妻率。
3. 代碼實現(xiàn)存取錢(未加鎖)
- (void)moneyTest {
    self.moneyCount = 1000;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 10; i++) {
        [self drawMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 10; i++) {
        [self saveMoney];
        }
    });
    
}

- (void)drawMoney {
    int readCount = self.moneyCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount - 100;
    self.moneyCount = remainCount;
    NSLog(@"存錢---%@---剩余:%d",[NSThread currentThread], remainCount);
}

- (void)saveMoney {
    int readCount = self.moneyCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount + 200;
    self.moneyCount = remainCount;
    NSLog(@"取錢---%@---剩余:%d",[NSThread currentThread], remainCount);
}
  • 運行代碼乱顾,可以看到最終錢總不等于 2000 元
4. 代碼實現(xiàn)賣票(未加鎖)

- (void)ticketTest {
    self.ticketsCount = 20;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket {
    int readCount = self.ticketsCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount - 1;
    self.ticketsCount = remainCount;
    NSLog(@"賣票---%@---剩余:%d",[NSThread currentThread], remainCount);
}
  • 運行代碼看到,最終票的總數(shù)經(jīng)常不等于 0
5. 代碼實現(xiàn) 賣票 和 存取錢(加自旋鎖)
#import "ViewController.h"
#import <libkern/OSAtomic.h>

OSSpinLock moneyLock = OS_SPINLOCK_INIT;
OSSpinLock ticketsLock = OS_SPINLOCK_INIT;

@interface ViewController ()
@property(nonatomic, assign) int moneyCount;
@property(nonatomic, assign) int ticketsCount;
@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    [self moneyTest];
//    [self ticketTest];
}
- (void)moneyTest {
    self.moneyCount = 1000;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 10; i++) {
        [self drawMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 10; i++) {
        [self saveMoney];
        }
    });
    
}

- (void)drawMoney {
    OSSpinLockLock(&moneyLock);
    int readCount = self.moneyCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount - 100;
    self.moneyCount = remainCount;
    NSLog(@"存錢---%@---剩余:%d",[NSThread currentThread], remainCount);
    OSSpinLockUnlock(&moneyLock);
}

- (void)saveMoney {
    OSSpinLockLock(&moneyLock);
    int readCount = self.moneyCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount + 200;
    self.moneyCount = remainCount;
    NSLog(@"取錢---%@---剩余:%d",[NSThread currentThread], remainCount);
    OSSpinLockUnlock(&moneyLock);
}

- (void)ticketTest {
    self.ticketsCount = 20;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket {
    OSSpinLockLock(&ticketsLock);
    int readCount = self.ticketsCount;
    sleep(0.2); //模擬網(wǎng)絡(luò)擁堵
    int remainCount = readCount - 1;
    self.ticketsCount = remainCount;
    NSLog(@"賣票---%@---剩余:%d",[NSThread currentThread], remainCount);
    OSSpinLockUnlock(&ticketsLock);
}
@end

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宫静,一起剝皮案震驚了整個濱河市走净,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌孤里,老刑警劉巖伏伯,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捌袜,居然都是意外死亡说搅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門虏等,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弄唧,“玉大人霍衫,你說我怎么就攤上這事候引。” “怎么了敦跌?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵背伴,是天一觀的道長。 經(jīng)常有香客問我峰髓,道長傻寂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任携兵,我火速辦了婚禮疾掰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘徐紧。我一直安慰自己静檬,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布并级。 她就那樣靜靜地躺著拂檩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嘲碧。 梳的紋絲不亂的頭發(fā)上稻励,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼望抽。 笑死加矛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的煤篙。 我是一名探鬼主播斟览,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辑奈!你這毒婦竟也來了苛茂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鸠窗,失蹤者是張志新(化名)和其女友劉穎味悄,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塌鸯,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡侍瑟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了丙猬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涨颜。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茧球,靈堂內(nèi)的尸體忽然破棺而出庭瑰,到底是詐尸還是另有隱情,我是刑警寧澤抢埋,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布弹灭,位于F島的核電站,受9級特大地震影響揪垄,放射性物質(zhì)發(fā)生泄漏穷吮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一饥努、第九天 我趴在偏房一處隱蔽的房頂上張望捡鱼。 院中可真熱鬧,春花似錦酷愧、人聲如沸驾诈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乍迄。三九已至,卻和暖如春士败,著一層夾襖步出監(jiān)牢的瞬間闯两,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留生蚁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓戏自,卻偏偏與公主長得像邦投,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子擅笔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355