多線程
官方文檔:線程編程指南
GCD源碼:https://github.com/apple/swift-corelibs-libdispatch
iOS 中常見的多線程方案
iOS 中常將的多線程方案如下:
GCD 多線程基本概念
- 同步/異步: 是否有開啟新線程的能力
- 串行隊(duì)列/并行隊(duì)列: 是否具有并行執(zhí)行任務(wù)的能力乌逐。主隊(duì)列是一種特殊的串行隊(duì)列
Note:
在主線程執(zhí)行同步串行任務(wù)竭讳,會(huì)卡死主線程。
原理: 在串行隊(duì)列里面執(zhí)行同步任務(wù)浙踢,就會(huì)產(chǎn)生死鎖绢慢。
多線程安全問題與解決
多線程安全問題在于:多個(gè)線程同時(shí)訪問并修改同一變量值,會(huì)造成最終值不正確洛波。
例如:存取錢問題呐芥、售票問題
解決方案: 使用線程同步技術(shù)(就是協(xié)同步調(diào),按照預(yù)定的先后次序進(jìn)行)奋岁。常見的線程同步技術(shù)為:加鎖
原則: 對(duì)于修改同一個(gè)變量值,需要用同一個(gè)鎖。如果只是讀取荸百,則無需加鎖
常用的鎖(效率從高到底):
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
OSSpinLock
OSSpinLock 自旋鎖, 等待鎖的線程會(huì)處于忙等狀態(tài)(busy-wait),一直占用著CPU資源
目前已經(jīng)不再安全闻伶,可能會(huì)出現(xiàn)線程優(yōu)先級(jí)翻轉(zhuǎn)問題。表現(xiàn)上也類似死鎖:如果等待鎖的線程優(yōu)先級(jí)較高够话,它就會(huì)一直占用CPU資源蓝翰,優(yōu)先級(jí)低的線程就無法釋放鎖
已經(jīng)在iOS10開始被廢棄光绕。需要引入頭文件#import <libkern/OSAtomic.h>
,使用如下:
#import <libkern/OSAtomic.h>
// 初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT;
// 加鎖
OSSpinLockLock(&lock);
// 中間需要做的操作...
// 解鎖
OSSpinLockUnlock(&lock);
/////////////////////////////////////////////
iOS 10之后替代 os_unfair_lock 頭文件<os/lock.h>
os_unfair_lock
os_unfair_lock 作為 OSSpinLock 的替代品,解決了優(yōu)先級(jí)反轉(zhuǎn)問題畜份,能做到讓等待的線程處于真正的休眠狀態(tài)诞帐,其接口與OSSpinLock 相似。需導(dǎo)入頭文文件<os/lock.h>
#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 加鎖/嘗試加鎖
void os_unfair_lock_lock(os_unfair_lock_t lock);
bool os_unfair_lock_trylock(os_unfair_lock_t lock);
// 解鎖
void os_unfair_lock_unlock(os_unfair_lock_t lock);
// 判斷是否鎖的擁有者是自己爆雹,
void os_unfair_lock_assert_owner(os_unfair_lock_t lock);
void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);
pthread_mutex
pthread_mutex 能做到讓等待的線程處于休眠狀態(tài)停蕉。需要引入頭文件 <pthread.h>
互斥鎖/遞歸鎖/條件鎖
// 普通互斥鎖,屬性傳NULL
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
// 中間需要的操作
pthread_mutex_unlock(&_mutex);
---遞歸鎖 -----------------------
// 遞歸鎖:允許同一個(gè)線程對(duì)一把鎖進(jìn)行重復(fù)加鎖
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_init(mutex, &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&_mutex);
// 中間需要的操作
pthread_mutex_unlock(&_mutex);
----條件鎖--------------------------
當(dāng)多線程執(zhí)行任務(wù)有條件依賴的是可以用條件鎖钙态。
- (void)__remove
{
pthread_mutex_lock(&_mutex);
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
[self.data removeLastObject];
pthread_mutex_unlock(&_mutex);
}
// 線程2
// 往數(shù)組中添加元素
- (void)__add
{
pthread_mutex_lock(&_mutex);
sleep(1);
[self.data addObject:@"Test"];
// 信號(hào) - 喚醒被該條件加的鎖
pthread_cond_signal(&_cond);
// 廣播
// pthread_cond_broadcast(&_cond);
pthread_mutex_unlock(&_mutex);
}
NSLock慧起、NSRecursiveLock、NSCondition册倒、NSConditionLock
這幾個(gè)鎖是基于 pthread_mutex 的 OC 封裝蚓挤。其使用更加簡單、更加面向?qū)ο蟆?/p>
// NSLock - 封裝自 pthread_mutex_lock 默認(rèn)鎖
self.lock = [[NSLock alloc] init];
[self.ticketLock lock];
// 加鎖代碼
[self.ticketLock unlock];
// NSCondition -- 封裝自 pthread_mutex_lock 默認(rèn)條件鎖
self.condition = [[NSCondition alloc] init];
[self.condition lock];
// 等待
[self.condition wait];
// 信號(hào)
[self.condition signal];
// 廣播
[self.condition broadcast];
[self.condition unlock];
// NSConditionLock -- 封裝自 pthread_mutex_lock 條件鎖驻子,可加自定義條件
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
// 以下三段代碼可以按順序執(zhí)行
[self.conditionLock lock];
NSLog(@"__one");
[self.conditionLock unlockWithCondition:2];
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];
[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
[self.conditionLock unlock];
//
dispatch_queue(DISPATCH_QUEUE_SERIAL)
使用串行隊(duì)列也能解決多線程資源競爭問題灿意,將線程加入到串行隊(duì)列按順序執(zhí)行。
self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.serialQueue, ^{
// 處理變量賦值等核心功能代碼
});
dispatch_semaphore
semaphore 叫做“信號(hào)量”,用來控制線程的最大并發(fā)數(shù)量崇呵。
如果信號(hào)量的值 > 0缤剧,就讓信號(hào)量的值減1,然后繼續(xù)往下執(zhí)行代碼演熟。
如果信號(hào)量的值 <= 0鞭执,就會(huì)休眠等待,直到信號(hào)量的值變成>0芒粹,就讓信號(hào)量的值減1兄纺,然后繼續(xù)往下執(zhí)行代碼。
dispatch_semaphore_signal(); 給對(duì)對(duì)應(yīng)的信號(hào)量 +1
semaphore 初始值為1時(shí)候化漆,非常適合做線程同步
// 設(shè)置最大允許并發(fā)數(shù) 5
self.semaphore = dispatch_semaphore_create(5);
- (void)test
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 相關(guān)代碼
// 讓信號(hào)量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@synchronized
@synchronized 是對(duì) mutex 遞歸鎖的封裝估脆。可以參考 runtime 源碼 objc_sync源碼座云。
// 參數(shù)即要設(shè)置為鎖的值疙赠,就是一個(gè)指針
@synchronized([self class]) {
[super __drawMoney];
}
鎖的使用小技巧: 宏
#define SemaphoreBegin \
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);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
--------------
SemaphoreBegin;
// .....
SemaphoreEnd;
atomic 原子操作
寫屬性的時(shí)候常用 atomic、nonatomic
給屬性加上 atomic 修飾朦拖,可以保證屬性 setter 和 getter 方法都是原子性操作圃阳,也就是保證 setter 和 getter 內(nèi)部都是線程同步的。這里可以參考 runtime 源碼 objc-accessors
璧帝。本質(zhì)上也是加鎖捍岳,源碼如下
// 獲取屬性對(duì)象
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) { // 對(duì)象本質(zhì)為結(jié)構(gòu)體,根據(jù)屬性的在結(jié)構(gòu)體內(nèi)的 offset 獲取。如果offset == 0 即獲取結(jié)構(gòu)體首地址锣夹,即 isa 地址
return object_getClass(self);
}
// Retain release world
// 根據(jù) offset 獲取結(jié)構(gòu)體內(nèi) 屬性指針
id *slot = (id*) ((char*)self + offset);
// 如果是非原子屬性页徐,就直接返回屬性指針
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
atomic 給 setter/getter 內(nèi)部加鎖保證了屬性存取的安全,但是不能保證屬性取出之后的操作安全银萍。
因?yàn)榇嫒》椒ㄊ褂眠^于頻繁变勇,所以 atomic 顯得過于消耗性能。
iOS 中的讀寫安全方案
IO 操作 -> 文件讀寫操作 -> 【多度贴唇,單寫】
實(shí)際操作條件:
- 同一時(shí)間搀绣,只能有一個(gè)線程進(jìn)行寫操作
- 同一時(shí)間,允許有多個(gè)線程進(jìn)行讀操作
- 同一時(shí)間滤蝠,不允許既有寫操作豌熄,又有讀操作
方案如下:
- pthread_rwlock
- dispatch_barrier_sync
pthread_rwlock
pthread_rwlock 也是互斥鎖,等待鎖的進(jìn)程會(huì)進(jìn)入休眠物咳。使用如下
// 創(chuàng)建讀寫鎖屬性
pthread_rwlockattr_t rwAttr;
pthread_rwlockattr_init(&rwAttr);
// 初始化鎖
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, &rwAttr); // pthread_rwlock_init(&lock, NULL);
// 讀-加鎖
pthread_rwlock_rdlock(&lock);
// 讀-嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
// 寫-加鎖
pthread_rwlock_wrlock(&lock);
// 寫-嘗試加鎖
pthread_rwlock_trywrlock(&lock);
// 解鎖
pthread_rwlock_unlock(&lock);
// 銷毀
pthread_rwlock_destroy(&lock);
dispatch_barrier_sync
- 這個(gè)函數(shù)闖入的并發(fā)隊(duì)列锣险,必須是自己通過
dispatch_queue_create
創(chuàng)建的 - 如果傳入的是一個(gè)串行或者全局并發(fā)隊(duì)列,那就相當(dāng)于調(diào)用
dispatch_async
函數(shù)
// 創(chuàng)建隊(duì)列
dispatch_queue_t _Nonnull queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 讀 - 異步線程览闰,可以多線程同時(shí)訪問
dispatch_barrier_async(queue, ^{
});
// 寫 - 同步任務(wù)芯肤,只有一個(gè)線程可以寫
dispatch_barrier_sync(queue, ^{
});
面試題
- 你理解的多線程?
線程是應(yīng)用程序內(nèi)部實(shí)現(xiàn)多個(gè)執(zhí)行路徑的相對(duì)輕量的方法压鉴。
系統(tǒng)->并行執(zhí)行進(jìn)程->進(jìn)程執(zhí)行一個(gè)或者多個(gè)線程崖咨。
這些線程可以同時(shí)或者幾乎同時(shí)的方式執(zhí)行不同的任務(wù)。
系統(tǒng)本身實(shí)際上管理這些執(zhí)行的線程油吭,安排它們?cè)诳捎玫膬?nèi)核上運(yùn)行击蹲,并根據(jù)需要中斷它們,將執(zhí)行時(shí)間分配給其他線程婉宰。
多線程有點(diǎn):
1. 可以提高程序的感知響應(yīng)能力歌豺,
2. 可以提高應(yīng)用程序在多核系統(tǒng)上的實(shí)時(shí)性能
缺點(diǎn):
1. 增加代碼復(fù)雜性,它們可以訪問同樣的資源心包,多個(gè)線程需協(xié)同合作类咧,防止破壞程序的狀態(tài)信息
2. 線程間的資源競爭問題,需要線程同步的技術(shù)來額外處理
- 以下代碼執(zhí)行情況如何?正確執(zhí)行/奔潰蟹腾?why?
- (void)interview01
{
// 會(huì)產(chǎn)生死鎖痕惋!卡死主線程
NSLog(@"執(zhí)行任務(wù)1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"執(zhí)行任務(wù)2");
});
NSLog(@"執(zhí)行任務(wù)3");
// dispatch_sync立馬在當(dāng)前線程同步執(zhí)行任務(wù)
}
- (void)interview02
{
// 問題:以下代碼是在主線程執(zhí)行的,會(huì)不會(huì)產(chǎn)生死鎖娃殖?不會(huì)值戳!
NSLog(@"執(zhí)行任務(wù)1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"執(zhí)行任務(wù)2");
});
NSLog(@"執(zhí)行任務(wù)3");
// dispatch_async不要求立馬在當(dāng)前線程同步執(zhí)行任務(wù)
}
- (void)interview03
{
// 問題:以下代碼是在主線程執(zhí)行的,會(huì)不會(huì)產(chǎn)生死鎖炉爆?會(huì)述寡!
NSLog(@"執(zhí)行任務(wù)1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"執(zhí)行任務(wù)2");
dispatch_sync(queue, ^{ // 1
NSLog(@"執(zhí)行任務(wù)3");
});
NSLog(@"執(zhí)行任務(wù)4");
});
NSLog(@"執(zhí)行任務(wù)5");
}
- (void)interview04
{
// 問題:以下代碼是在主線程執(zhí)行的柿隙,會(huì)不會(huì)產(chǎn)生死鎖?不會(huì)鲫凶!
NSLog(@"執(zhí)行任務(wù)1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"執(zhí)行任務(wù)2");
dispatch_sync(queue2, ^{ // 1
NSLog(@"執(zhí)行任務(wù)3");
});
NSLog(@"執(zhí)行任務(wù)4");
});
NSLog(@"執(zhí)行任務(wù)5");
}
- (void)interview05
{
// 問題:以下代碼是在主線程執(zhí)行的,會(huì)不會(huì)產(chǎn)生死鎖衩辟?不會(huì)螟炫!
NSLog(@"執(zhí)行任務(wù)1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"執(zhí)行任務(wù)2");
dispatch_sync(queue, ^{ // 1
NSLog(@"執(zhí)行任務(wù)3");
});
NSLog(@"執(zhí)行任務(wù)4");
});
NSLog(@"執(zhí)行任務(wù)5");
}
- 下面代碼打印什么?為什么艺晴?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self test2];
}
- (void)test
{
NSLog(@"2");
}
- (void)test2
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}
// 打印 1昼钻、3
// 原因: performSelector:withObject:afterDelay 這個(gè)方法本質(zhì)上是給Runloop 添加定時(shí)器。而子線程雖然已經(jīng)創(chuàng)建了 runloop 但是并沒有運(yùn)行封寞,所以不會(huì)打印然评,處理方式就是運(yùn)行子線程的 runloop,讓子線程北肪浚活
// 以下代碼同理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- 如何實(shí)現(xiàn)如首頁多個(gè)網(wǎng)絡(luò)請(qǐng)求碗淌,最后一個(gè)請(qǐng)求基于前面的網(wǎng)絡(luò)請(qǐng)求的情況
// 使用 dispatch_group 的方式。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 創(chuàng)建隊(duì)列組
dispatch_group_t group = dispatch_group_create();
// 創(chuàng)建并發(fā)隊(duì)列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
// 添加異步任務(wù)
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]);
}
});
// 等前面的任務(wù)執(zhí)行完畢后抖锥,會(huì)自動(dòng)執(zhí)行這個(gè)任務(wù)
// dispatch_group_notify(group, queue, ^{
// dispatch_async(dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
// }
// });
// });
// dispatch_group_notify(group, dispatch_get_main_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ù)3-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任務(wù)4-%@", [NSThread currentThread]);
}
});
}
- iOS 的多線程有幾種方案亿眠,你更傾向于哪一種?
pthread
NSThread
GCD ---> 更傾向
NSOperation
- 你在項(xiàng)目中用過 GCD 嗎磅废?
用過
如:
dispatch_semaphore -> 信號(hào)量
dispatch_barrier
dispatch_queue
dispatch_group
dispatch_sync & dispatch_async
...
- GCD 的隊(duì)列類型
串行隊(duì)列 & 并行隊(duì)列
- 說一下 OperationQueue 和 GCD 的區(qū)別纳像,以及各自優(yōu)勢(shì)?
GCD:
基于C語言的API拯勉,旨在替代 NSThread 的線程技術(shù)竟趾,可以高效利用設(shè)備多核。
OperationQueue:
底層封裝自 GCD宫峦,增加了很多使用功能岔帽,更加面向?qū)ο蟆?
- 線程安全處理的手段有哪些?
1. 加鎖
2. 使用 GCD 串行隊(duì)列
3. 使用 GCD 信號(hào)量
- OC 你了解的鎖有哪些斗遏?在你的回答基礎(chǔ)上進(jìn)行二次提問
- 1.自旋鎖和互斥鎖的對(duì)比
- 2.使用以上鎖需要注意哪些山卦?
- 3.使用 C/OC/C++,任選其一,實(shí)現(xiàn)自旋或互斥诵次?口述即可
了解的鎖:
OSSpinLock账蓉、os_unfair_lock、pthread_mutex逾一、NSLock铸本、NSCondition、NSRecursiveLock遵堵、NSConditionLock箱玷、@synchronized
自旋鎖適合的場景
1. 預(yù)計(jì)線程等待鎖的時(shí)間很短
2. 加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用怨规,但競爭情況不是很激烈
3. CPU 資源不是很緊張
4. 多核處理器
互斥鎖比較適合的場景
1. 預(yù)計(jì)線程等待的時(shí)間較長
2. 單核處理器(減少CPU占用)
3. 臨界區(qū)有 IO 操作(IO 操作本身占CPU)
4. 臨界區(qū)代碼復(fù)雜或者循環(huán)量很大
5. 臨界區(qū)競爭激烈