iOS 多線程總結(jié)(下)

一、前言

繼續(xù)我們上篇《iOS 多線程總結(jié)(上)》,繼續(xù)總結(jié)多線程的其他知識點,希望幫助到更多伙伴。這篇主要總結(jié)一下線程同步方案,atomic 以及讀寫安全方案。

二、iOS 中的線程同步方案

  • 線程同步的意思就是讓多線程的操作按順序執(zhí)行。
    方案有如下10 種:
    \color{blue}{OSSpinLock}自旋鎖
    os_unfair_lock 自旋鎖的替代品
    pthread_mutex
    dispatch_semaphore 信號量
    dispatch_queue(DISPATCH_QUEUE_SERIAL)串行隊列
    \color{green}{NSLock}
    \color{green}{NSRecursiveLock}
    \color{green}{NSCondition}
    \color{green}{NSConditionLock} 條件鎖
    \color{red}{@synchronized}

多個線程修改某個方法中同一個變量時,要用全局變量的鎖,每個線程執(zhí)行到這個方法時,會判斷這個鎖是否被加鎖了,如果被加鎖了則會等待鎖被解鎖再繼續(xù)執(zhí)行,所以必須使用全局變量的鎖。
如果只在一個方法里使用了這把鎖,也可以做成 static 類型的,這樣也可以達到只初始化一次的效果。

1、OSSpinLock (自旋鎖)

  • \color{purple}{OSSpinLock} 叫做”自旋鎖“,等待鎖的線程會處于忙等(busy-wait)狀態(tài),一直占用著 CPU 資源。
  • 目前已經(jīng)不再安全,可能出現(xiàn)優(yōu)先級反轉(zhuǎn)問題。
  • 需要導入頭文件<libkern/OSAtomic.h>

讓線程停止有兩種方法:
1、一直 while 判斷等待,忙等;
2、sleep 休眠的方式;

\color{purple}{OSSpinLock} 自旋鎖的優(yōu)先級反轉(zhuǎn)問題:

  • 如果等待鎖的線程優(yōu)先級較高,它會一直占用著 CPU 資源,優(yōu)先級低的線程就無法釋放。
    例子:
    有兩個線程,
    thread1:優(yōu)先級比較高
    thread2:優(yōu)先級比較低
    如果優(yōu)先級比較低的 thread2 先進了方法給鎖進行了加鎖,緊接著優(yōu)先級比較高的 thread1 也進來這個方法了,發(fā)現(xiàn)這個鎖已經(jīng)被被人加過了,thread1 只能忙等,由于 thread1 的優(yōu)先級比較高,cpu 就有可能一直在分配時間給 thread1,cpu 就沒有時間再分配給 thread2 了,這時 thread2 的代碼就沒辦法繼續(xù)執(zhí)行,就永遠無法解鎖,thread1 就會一直在等,類似于死鎖了的感覺了。所以自旋鎖有可能會有這種問題。
    如果采用休眠的方式的鎖,則不會產(chǎn)生這個問題。

2、os_unfair_lock(互斥鎖)

當我們使用 OSSpinLock 時,現(xiàn)在會報下面這個警告:

  • iOS10 開始,蘋果希望我們使用 os_unfair_lock 來代替 OSSpinLock。
  • 從底層調(diào)用看,等待 os_unfair_lock 鎖的線程會處于休眠狀態(tài),并非忙等。
  • 需要導入頭文件<os/lock.h>

從蘋果的注釋可以看到,這是個Low-level lock(簡稱ll lock 或lll),低級鎖,低級鎖特點就是等不到鎖就休眠。

3、pthread_mutex 普通鎖

  • mutex 叫做”互斥鎖“,等待鎖的線程會處于休眠狀態(tài)
    pthread 開頭的一般都是跨平臺使用的鎖。
  • 需要導入 <pthread.h>

屬性默認是 PTHREAD_MUTEX_NORMAL,屬性傳空時也是這個默認的。
當屬性傳 PTHREAD_MUTEX_RECURSIVE 時,是遞歸鎖。
遞歸鎖:
允許\color{red}{同一個線程}對一把鎖進行重復加鎖。

  • 自旋鎖原理

執(zhí)行斷點的時候,控制臺輸入step是一行一行 OC 代碼執(zhí)行(默認),如果輸入stepi(縮寫si也行),就是一行一行匯編指令執(zhí)行。
nexti也是一行一行匯編執(zhí)行,區(qū)別是遇到函數(shù)callq的時候,會一筆帶過,不會進入函數(shù)。而 stepi 會進入函數(shù)。輸入continue(縮寫c也行)可以直接到下一個你打的斷點。
輸入完指令,輸入回車即可。
在多個線程使用 OSSpinLock 自旋鎖時,可以看到匯編代碼一直在執(zhí)行 0x111126a320x111126a41 這段代碼,0x111126a43 的jne是jump判斷,如果符合條件,則跳轉(zhuǎn)到0x111126a32,這就是一個 while 循環(huán),在忙等。

4、pthread_mutex - 遞歸鎖

5、pthread_mutex - 條件鎖

例子:
對一個數(shù)組的刪除和添加操作分別在兩個子線程,但是刪除數(shù)組元素之前,我們需要先添加元素。

@interface NSConditionDemo()
@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation NSConditionDemo

- (instancetype)init {
    if (self = [super init]) {
        self.condition = [[NSCondition alloc] init];
        self.data = [NSMutableArray array];
    }
    return self;
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 生產(chǎn)者-消費者模式

// 線程1
// 刪除數(shù)組中的元素
- (void)__remove {
    [self.condition lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    [self.condition unlock];
}

// 線程2
// 往數(shù)組中添加元素
- (void)__add {
    [self.condition lock];
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    // 信號
    [self.condition signal];
    // 廣播
//    [self.condition broadcast];
    [self.condition unlock];
}

使用場景:
多線程之間的依賴問題。比如線程1依賴線程2做一些事情,再回到線程1做事情。

5、NSLock、NSRecursiveLock

\color{purple}{NSLock} 是對 C 語言mutex 普通鎖的 OC 版本的封裝。就是對 pthread_mutex 的屬性傳 PTHREAD_MUTEX_NORMAL 時的封裝。

NSLock的方法

tryLock 是嘗試加鎖,并且不阻塞。
lockBeforeDate 是如果在某個時間前可以加鎖成功,則加鎖并返回YES,否則返回NO,阻塞。

NSRecursiveLock 也是對 mutex 遞歸鎖的OC版本的封裝,API 跟 NSLock 基本一致。
所以 mutex 和 NSLock 的性能其實是一樣的,只不過 NSLock 是面向?qū)ο蟮亩选?/p>

6、NSCondition 條件鎖

\color{purple}{NSCondition} 是對 C 語言的 pthread_mutex_tpthread_cond_tOC 面向?qū)ο蟮姆庋b,既包含了鎖,也包含了條件。

image.png

7、NSConditionLock 條件鎖

\color{purple}{NSConditionLock} 是對 NSCondition 的進一步封裝,可以設置具體的條件值。

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation NSConditionLockDemo

- (instancetype)init {
    if (self = [super init]) {
        self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
}

- (void)__one {
    [self.conditionLock lock];
    NSLog(@"__one");
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
}

- (void)__two {
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    sleep(1);
    [self.conditionLock unlockWithCondition:3];
}

- (void)__three {
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"__three");
    [self.conditionLock unlock];
}

上面代碼就是 __three 依賴于__two,__two 依賴于__one。
所以當我們希望不同子線程之間是有順序的時候,也可以用條件鎖實現(xiàn)。

8、dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行隊列

直接使用 GCD 的串行隊列,也是可以實現(xiàn)線程同步的。


例子:

@interface SerialQueueDemo()
@property (strong, nonatomic) dispatch_queue_t ticketQueue;
@property (strong, nonatomic) dispatch_queue_t moneyQueue;
@end

@implementation SerialQueueDemo

- (instancetype)init {
    if (self = [super init]) {
        self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
        self.moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)__drawMoney {
    dispatch_sync(self.moneyQueue, ^{
        [super __drawMoney];
    });
}

- (void)__saveMoney {
    dispatch_sync(self.moneyQueue, ^{
        [super __saveMoney];
    });
}

- (void)__saleTicket {
    dispatch_sync(self.ticketQueue, ^{
        [super __saleTicket];
    });
}

9、dispatch_semaphore

  • \color{purple}{semaphore} 叫做”信號量“;
  • 信號量的初始值,可以用來控制線程并發(fā)訪問的最大數(shù)量;
  • 信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步。


所以我們可以把線程最大并發(fā)數(shù)設置為1,這樣就可以達到線程同步的目的了。

10、@synchronized

  • \color{purple}{synchronized} 是對 mutex 遞歸鎖的封裝。
    從代碼簡潔度講,是最簡單的一種方案。但我們在xcode敲這個關鍵字的時候是沒有自動提示的,因為蘋果不推薦我們使用它,因為它的性能比較差。
  • 源碼查看:objc4 中的 objc-sync.mm 文件
    底層是一個哈希表,根據(jù)傳進去的對象作為key,找到封裝的mutex的唯一對應的一把鎖,大括號開始時是加鎖,大括號結(jié)束時解鎖。
- (void)__drawMoney {
    @synchronized([self class]) {
        [super __drawMoney];
    }
}

- (void)__saveMoney {
    @synchronized([self class]) { // objc_sync_enter
        [super __saveMoney];
    } // objc_sync_exit
}

- (void)__saleTicket {
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    
    @synchronized(lock) {
        [super __saleTicket];
    }
}

- (void)otherTest {
    @synchronized([self class]) {
        NSLog(@"123");
        [self otherTest];
    }
}

iOS 線程同步方案性能比較

  • 性能從高到低排序
    os_unfair_lock —— iOS10 以后才可以用
    OSSpinLock ——已經(jīng)不推薦使用
    dispatch_semaphore ——信號量
    pthread_mutex
    dispatch_queue(DISPATCH_QUEUE_SERIAL)——串行隊列
    NSLock ——對 pthread_mutex 的封裝
    NSCondition ——條件
    pthread_mutex(recursive) ——遞歸鎖
    NSRecursiveLock ——遞歸鎖,對 pthread_mutex(recursive) 的封裝
    NSConditionLock ——條件鎖
    @synchronized ——性能最差,但代碼最簡潔
    所以最推薦 dispatch_semaphorepthread_mutex

??小技巧:
使用信號量的時候,如果我們 3 個方法需要用不同的鎖,那么我們除了可以在外面定義三個不同的鎖屬性,還可以在每個方法內(nèi)部定義靜態(tài)的鎖,這樣就能保證一個方法一個鎖了。

static dispatch_semaphore_t semaphore;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    semaphore = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

// ... 要加鎖的代碼...

dispatch_semaphore_signal(semaphore);

更簡便的是進行宏定義:
- (void)test1 {
    SemaphoreBegin;
    // .....要加鎖的代碼...
    SemaphoreEnd;
}

- (void)test2 {
    SemaphoreBegin;
    // .....要加鎖的代碼...
    SemaphoreEnd;
}

- (void)test3 {
    SemaphoreBegin;
    // .....要加鎖的代碼...
    SemaphoreEnd;
}
  • 什么情況使用自旋鎖比較劃算?
    1、預計線程等待鎖的時間段;
    2、加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用,但競爭情況很少發(fā)生;
    3、CPU資源不緊張;
    4、多核處理器;

  • 什么情況使用互斥鎖比較劃算?
    1、預計線程等待鎖的時間較長;
    2、單核處理器;
    3、臨界區(qū)有IO操作;
    4、臨界區(qū)代碼復雜或者循環(huán)量大;
    5、臨界區(qū)競爭非常激烈(很多線程搶占資源);
    但在 iOS 里不考慮使用自旋鎖了,只用互斥鎖即可。

三、 atomic

nonatomic 和 atomic

atom:原子,不可再分割的單位
atomic:原子性

給屬性加上 atomic 修飾,可以保證屬性的 settergetter 都是原子性操作,也就是保證 settergetter 內(nèi)部是線程安全的。

可以通過 objc4 源碼中的 objc-accessors.mm 查看,objc_getPropertyreallySetProperty。
reallySetProperty 中:

objc_getProperty中

  • atomic 用于保證屬性 setter、getter 的原子性操作,相當于在 gettersetter 內(nèi)部加了線程同步的鎖。
  • 它不能保證使用屬性的過程是線程安全的。
    iOS 由于性能問題,一般不使用 atomic,在 mac OS 使用會更多些。

四、iOS 中的讀寫安全方案

如果實現(xiàn)以下場景:

  • 同一時間,只能有1個線程進行寫的操作
  • 同一時間,允許有多個線程進行讀的操作
  • 同一時間,不允許既有寫的操作,又有讀的操作
    上面的場景就是典型的”多讀單寫“,經(jīng)常用于文件等數(shù)據(jù)的讀寫操作,iOS中的實現(xiàn)方案有:
    pthread_rwlock讀寫鎖
    dispatch_barrier_async異步柵欄調(diào)用

pthread_rwlock

  • 等待鎖的線程會進入休眠


dispatch_barrier_async

  • 這個函數(shù)傳入的并發(fā)隊列必須是自己通過 dispatch_queue_cretate 創(chuàng)建的
  • 如果傳入的是一個串行或是一個全局的并發(fā)隊列,那這個函數(shù)便等同于dispatch_async 函數(shù)的效果

以上的總結(jié)參考了并部分摘抄了以下文章,非常感謝以下作者的分享!:
小馬哥-李明杰的《多線程》課程

轉(zhuǎn)載請備注原文出處,不得用于商業(yè)傳播——凡幾多

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

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

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