一、前言
繼續(xù)我們上篇《iOS 多線程總結(jié)(上)》,繼續(xù)總結(jié)多線程的其他知識點,希望幫助到更多伙伴。這篇主要總結(jié)一下線程同步方案,atomic 以及讀寫安全方案。
二、iOS 中的線程同步方案
-
線程同步的意思就是讓多線程的操作按順序執(zhí)行。
方案有如下10 種:
自旋鎖
os_unfair_lock 自旋鎖的替代品
pthread_mutex
dispatch_semaphore 信號量
dispatch_queue(DISPATCH_QUEUE_SERIAL)串行隊列
條件鎖
多個線程修改某個方法中同一個變量時,要用全局變量的鎖,每個線程執(zhí)行到這個方法時,會判斷這個鎖是否被加鎖了,如果被加鎖了則會等待鎖被解鎖再繼續(xù)執(zhí)行,所以必須使用全局變量的鎖。
如果只在一個方法里使用了這把鎖,也可以做成 static 類型的,這樣也可以達到只初始化一次的效果。
1、OSSpinLock (自旋鎖)
-
叫做”自旋鎖“,等待鎖的線程會處于忙等(busy-wait)狀態(tài),一直占用著 CPU 資源。
- 目前已經(jīng)不再安全,可能出現(xiàn)優(yōu)先級反轉(zhuǎn)問題。
- 需要導入頭文件
<libkern/OSAtomic.h>
讓線程停止有兩種方法:
1、一直 while 判斷等待,忙等;
2、sleep 休眠的方式;
自旋鎖的優(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 時,是遞歸鎖。
遞歸鎖:
允許對一把鎖進行重復加鎖。
- 自旋鎖原理
執(zhí)行斷點的時候,控制臺輸入step是一行一行 OC 代碼執(zhí)行(默認),如果輸入stepi(縮寫si也行),就是一行一行匯編指令執(zhí)行。
nexti也是一行一行匯編執(zhí)行,區(qū)別是遇到函數(shù)callq的時候,會一筆帶過,不會進入函數(shù)。而 stepi 會進入函數(shù)。輸入continue(縮寫c也行)可以直接到下一個你打的斷點。
輸入完指令,輸入回車即可。
在多個線程使用 OSSpinLock 自旋鎖時,可以看到匯編代碼一直在執(zhí)行 0x111126a32 到 0x111126a41 這段代碼,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
是對 C 語言mutex 普通鎖的 OC 版本的封裝。就是對 pthread_mutex 的屬性傳
PTHREAD_MUTEX_NORMAL 時的封裝。

tryLock 是嘗試加鎖,并且不阻塞。
lockBeforeDate 是如果在某個時間前可以加鎖成功,則加鎖并返回YES,否則返回NO,阻塞。
NSRecursiveLock 也是對 mutex 遞歸鎖的OC版本的封裝,API 跟 NSLock 基本一致。
所以 mutex 和 NSLock 的性能其實是一樣的,只不過 NSLock 是面向?qū)ο蟮亩选?/p>
6、NSCondition 條件鎖
是對 C 語言的 pthread_mutex_t 和 pthread_cond_t 的 OC 面向?qū)ο蟮姆庋b,既包含了鎖,也包含了條件。

7、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
-
叫做”信號量“;
- 信號量的初始值,可以用來控制線程并發(fā)訪問的最大數(shù)量;
-
信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步。
所以我們可以把線程最大并發(fā)數(shù)設置為1,這樣就可以達到線程同步的目的了。
10、@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_semaphore和pthread_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 修飾,可以保證屬性的 setter 和 getter 都是原子性操作,也就是保證 setter 和 getter 內(nèi)部是線程安全的。
可以通過 objc4 源碼中的 objc-accessors.mm 查看,objc_getProperty 和 reallySetProperty。
在reallySetProperty 中:

objc_getProperty中:

- atomic 用于保證屬性 setter、getter 的原子性操作,相當于在 getter 和 setter 內(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è)傳播——凡幾多





