iOS面試中多線程絕對(duì)是最重要的知識(shí)點(diǎn)之一,它在日常開發(fā)中會(huì)被廣泛使用嗤栓,而且多線程是有很多區(qū)分度很高的題目可供考察的佑刷。這篇文章會(huì)梳理下多線程和GCD相關(guān)的概念和幾個(gè)典型問題。因?yàn)镚CD相關(guān)的API用OC看著更直管一些钝计,所以這期實(shí)例就都用OC語(yǔ)言書寫恋博。
作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要私恬,這是一個(gè)我的iOS開發(fā)交流群:130595548债沮,不管你是大牛還是小白都?xì)g迎入駐 ,讓我們一起進(jìn)步本鸣,共同發(fā)展R唏谩(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔!)
概念篇
在面對(duì)一些我們常見的概念時(shí)荣德,我們常有種這個(gè)東西我熟隧土,就認(rèn)為自己理解了,其實(shí)這種程度是不夠的命爬。當(dāng)我們可以清晰準(zhǔn)確的向別人描述一個(gè)東西曹傀,并能理解其官方定義的每個(gè)用語(yǔ)的含義,才算是我們熟悉理解了它饲宛。所以這里單獨(dú)抽一節(jié)講下多線程中的概念皆愉。
進(jìn)程,線程艇抠,任務(wù)幕庐,隊(duì)列
進(jìn)程:資源分配的最小單位。在iOS中一個(gè)應(yīng)用的啟動(dòng)就是開啟了一個(gè)進(jìn)程家淤。 線程:CPU調(diào)度的最小單位异剥。一個(gè)進(jìn)程里會(huì)有多個(gè)線程。 大家可以思考下絮重,進(jìn)程和線程為什么是從資源分配和CPU調(diào)度層面進(jìn)行定義的冤寿。
任務(wù):每次執(zhí)行的一段代碼,比如下載一張圖片青伤,觸發(fā)一個(gè)網(wǎng)絡(luò)請(qǐng)求督怜。 隊(duì)列:隊(duì)列是用來組織任務(wù)的,一個(gè)隊(duì)列包含多個(gè)任務(wù)狠角。
GCD
GCD(Grand Central Dispatch)是異步執(zhí)行任務(wù)的技術(shù)之一号杠。開發(fā)者只需要定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)腄ispatch Queue中,GCD就能生成必要的線程執(zhí)行該任務(wù)。這里的線程管理是由系統(tǒng)處理的姨蟋,我們不必關(guān)心線程的創(chuàng)建銷毀屉凯,這大大方便了我們的開發(fā)效率。也可以說GCD是一種簡(jiǎn)化線程操作的多線程使用技術(shù)方案眼溶。
安卓沒有跟GCD完全相同的一套技術(shù)方案的悠砚,雖然它可以處理GCD實(shí)現(xiàn)的一系列效果。
串行偷仿,并行哩簿,并發(fā)
GCD的使用都是通過調(diào)度隊(duì)列(Dispatch Queue)的形式進(jìn)行的宵蕉,調(diào)度隊(duì)列有以下 幾種形式:
串行(serial):多任務(wù)中某時(shí)刻只能有一個(gè)任務(wù)被運(yùn)行酝静;
并行(parallel):相對(duì)于串行,某時(shí)刻有多個(gè)任務(wù)同時(shí)被執(zhí)行羡玛,需要多核能力别智;
并發(fā)(concurrent):引入時(shí)間片和搶占之后才有了并發(fā)的說法,某個(gè)時(shí)間片只有一個(gè)任務(wù)在執(zhí)行稼稿,執(zhí)行完時(shí)間片后進(jìn)行資源搶占薄榛,到下一個(gè)任務(wù)去執(zhí)行,即“微觀串行让歼,宏觀并發(fā)”敞恋,所以這種情況下只有一個(gè)空閑的某核,多核空閑就又可以實(shí)現(xiàn)并行運(yùn)行了谋右;
我們常用的調(diào)度隊(duì)列有以下幾種:
// 串行隊(duì)列
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
// 并發(fā)隊(duì)列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 全局并發(fā)隊(duì)列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 主隊(duì)列
let mainQueue = DispatchQueue.main
注意GCD創(chuàng)建的是并發(fā)隊(duì)列而不是并行隊(duì)列硬猫。但這里的并發(fā)隊(duì)列是一個(gè)相對(duì)寬泛的定義,它包含并行的概念改执,GCD作為一個(gè)智能的中心調(diào)度系統(tǒng)會(huì)根據(jù)系統(tǒng)情況判斷當(dāng)前能否使用多核能力分?jǐn)偠鄠€(gè)任務(wù)啸蜜,如果滿足的話此時(shí)就是在并行的執(zhí)行隊(duì)列中的任務(wù)。
同步辈挂,異步
同步:函數(shù)會(huì)阻塞當(dāng)前線程直到任務(wù)完成返回才能進(jìn)行其它操作衬横;
異步:在任務(wù)執(zhí)行完成之前先將函數(shù)值返回,不會(huì)阻塞當(dāng)前線程终蒂;
串行蜂林、并發(fā)和同步、異步相互結(jié)合能否開啟新線程
串行隊(duì)列 | 并發(fā)隊(duì)列 | 主隊(duì)列 | |
---|---|---|---|
同步 | 不開啟新線程 | 不開啟新線程 | 不開啟新線程 |
異步 | 開啟新線程 | 開啟新線程 | 不開啟新線程 |
主線程和主隊(duì)列
主線程是一個(gè)線程拇泣,主隊(duì)列是指主線程上的任務(wù)組織形式悉尾。
主隊(duì)列只會(huì)在主線程執(zhí)行,但主線程上執(zhí)行的不一定就是主隊(duì)列挫酿,還有可能是別的同步隊(duì)列构眯。因?yàn)榍罢f過,同步操作不會(huì)開辟新的線程早龟,所以當(dāng)你自定義一個(gè)同步的串行或者并行隊(duì)列時(shí)都是還在主線程執(zhí)行惫霸。
判斷當(dāng)前是否是主線程:
BOOL isMainThread = [NSThread isMainThread];
判斷當(dāng)前是否在主隊(duì)列上:
static void *mainQueueKey = "mainQueueKey";
dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL);
BOOL isMainQueue = dispatch_get_specific(mainQueueKey));
隊(duì)列與線程的關(guān)系
隊(duì)列是對(duì)任務(wù)的描述猫缭,它可以包含多個(gè)任務(wù),這是應(yīng)用層的一種描述壹店。線程是系統(tǒng)級(jí)的調(diào)度單位猜丹,它是更底層的描述。一個(gè)隊(duì)列(并行隊(duì)列)的多個(gè)任務(wù)可能會(huì)被分配到多個(gè)線程執(zhí)行硅卢。
問題
代碼分析
1射窒、分析下面代碼的執(zhí)行邏輯
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self syncMainTask];
}
- (void)syncMainTask {
dispatch_queue_main_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"main queue task");
});
}
這段代碼會(huì)輸出task1
,然后發(fā)生死鎖将塑,導(dǎo)致crash脉顿。
追加問題一:為什么會(huì)死鎖?死鎖就會(huì)導(dǎo)致crash点寥?
我們先分析crash的情況艾疟,正常死鎖應(yīng)該就是卡死的情況,不應(yīng)該導(dǎo)致carsh敢辩。那為什么會(huì)carsh呢蔽莱,看崩潰信息:
是一個(gè)EXC_BAD_INSTRUCTION
類型的crash,執(zhí)行了一個(gè)出錯(cuò)的命令戚长。
然后看__DISPATCH_WAIT_FOR_QUEUE__
的調(diào)用棧信息:
右側(cè)匯編代碼給出了更詳細(xì)的crash信息:BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread
盗冷。
在當(dāng)前線程已經(jīng)擁有的隊(duì)列中執(zhí)行dispatch_sync
同步操作會(huì)導(dǎo)致crash。
在libdispatch
的源碼中我們可以找到該函數(shù)的定義:
DISPATCH_NOINLINE
static void
__DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq) {
uint64_t dq_state = _dispatch_wait_prepare(dq);
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
"dispatch_sync called on queue "
"already owned by current thread");
}
/*...*/
}
所以我們知道了同廉,這個(gè)carsh是libdispatch
內(nèi)部拋出的仪糖,當(dāng)它檢測(cè)到可能發(fā)生死鎖時(shí),就直接觸發(fā)崩潰恤溶,事實(shí)上它不能完全判斷出所有死鎖的情況乓诽。
我們分析這里為什么會(huì)發(fā)生死鎖。首先syncMainTask
就是在主隊(duì)列中的咒程,我們?cè)谥麝?duì)列先添加dispatch_sync
然后再添加其內(nèi)部的block鸠天。主隊(duì)列FIFO,只有sync執(zhí)行完了才會(huì)執(zhí)行內(nèi)部的block帐姻,而此時(shí)是一個(gè)同步隊(duì)列稠集,block執(zhí)行完才會(huì)退出sync,所以導(dǎo)致了死鎖饥瓷。
對(duì)于死鎖的解釋我也查了好幾篇文章剥纷,有些說法其實(shí)是經(jīng)不起推敲的,這個(gè)解釋是我認(rèn)為相對(duì)合理的呢铆。
附一篇參考文章:GCD死鎖
引出問題二:什么情況下會(huì)發(fā)生死鎖晦鞋?
GCD中發(fā)生死鎖需要滿足兩個(gè)條件:
- 同步執(zhí)行串行隊(duì)列
- 執(zhí)行sync的隊(duì)列和block所在隊(duì)列為同一個(gè)隊(duì)列
引出問題三:如何避免死鎖?這段代碼應(yīng)該如何修改?
根據(jù)上面提到的條件悠垛,我們可以將任務(wù)異步執(zhí)行线定,或者換成一個(gè)并發(fā)隊(duì)列。另外將block放到一個(gè)非主隊(duì)列里執(zhí)行也是可以的确买。
2斤讥、分析一下代碼執(zhí)行結(jié)果
int a = 0;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
while (a < 2) {
dispatch_async(queue, ^{
a++;
});
}
NSLog(@"a = %d", a);
首先該段代碼會(huì)編譯不過,編譯器檢測(cè)到變量a
被block截獲湾趾,并嘗試修改就報(bào)以下錯(cuò)誤:
Variable is not assignable (missing __block type specifier)
芭商。如果我們要在block里對(duì)外界變量重新復(fù)制,需要添加__block
的聲明:__block int a = 0;
我們分析這段代碼搀缠,在開始while之后加入一個(gè)異步任務(wù)铛楣,再之后呢,這個(gè)是不確定了胡嘿,可能是執(zhí)行a++
也可能是因不滿足退出條件再次執(zhí)行加入異步任務(wù)蛉艾,直到滿足a<2
才會(huì)退出while循環(huán)钳踊。那輸出結(jié)果也就是不確定了衷敌,因?yàn)榭赡茉谂袛嗵鲅h(huán)和輸出結(jié)果的時(shí)候另外的線程又執(zhí)行了一次a++
。
再擴(kuò)展下拓瞪,如果將那個(gè)并發(fā)隊(duì)列改成主隊(duì)列缴罗,執(zhí)行邏輯還是一樣的嗎?
首先主隊(duì)列是不會(huì)開啟新線程的祭埂,主隊(duì)列上的異步操作執(zhí)行時(shí)機(jī)是等別的任務(wù)都執(zhí)行完了面氓,再來執(zhí)行添加的a++
。顯然在while循環(huán)里蛆橡,主隊(duì)列既有任務(wù)還未執(zhí)行完畢舌界,所以就不會(huì)執(zhí)行a++
,也就導(dǎo)致while循環(huán)不會(huì)退出泰演,形成死循環(huán)呻拌。
其它問題
什么是線程安全,為什么UI操作必須在主線程執(zhí)行
線程安全:當(dāng)多個(gè)線程訪問某個(gè)方法時(shí)睦焕,不管你通過怎樣的調(diào)用方式或者說這些線程如何交替的執(zhí)行藐握,我們?cè)谥鞒绦蛑胁恍枰プ鋈魏蔚耐剑@個(gè)類的結(jié)果行為都是我們?cè)O(shè)想的正確行為垃喊,那么我們就可以說這個(gè)類時(shí)線程安全的猾普。
為什么UI操作必須放到主線程:首先UIKit不是線程安全的,多線程訪問會(huì)導(dǎo)致UI效果不可預(yù)期本谜,所以我們不能使用多個(gè)線程去處理UI初家。那既然要單線程處理UI為什么是在主線程呢,這是因?yàn)閁IApplication作為程序的起點(diǎn)是在主線程初始化的,所以我們后續(xù)的UI操作也都要放到主線程處理溜在。
關(guān)于這個(gè)問題展開討論可以參閱這篇文章:iOS拾遺——為什么必須在主線程操作UI
開啟新的線程有哪些方法
1评架、NSThread
2、NSOperationQueue
3炕泳、GCD
4纵诞、NSObject的performSelectorInBackground
方法
5、pthread
多線程任務(wù)要實(shí)現(xiàn)順序執(zhí)行有哪些方法
1培遵、dispatch_group
2浙芙、dispatch_barrier
3、dispatch_semaphore_t
4籽腕、NSOperation的addDependency方法
如何實(shí)現(xiàn)一個(gè)多讀單寫的功能嗡呼?
多讀單寫的意思就是可以有多個(gè)線程同時(shí)參與讀取數(shù)據(jù),但是寫數(shù)據(jù)時(shí)不能有讀操作的參與切只有一個(gè)線程在寫數(shù)據(jù)皇耗。
我們寫一個(gè)示例程序南窗,看下在不做限制的多讀多寫程序中會(huì)發(fā)生什么。
// 計(jì)數(shù)器
self.count = 0;
// 并發(fā)隊(duì)列
self.concurrentQueue = dispatch_get_global_queue(0, 0);
for (int i = 0; i< 10; i++) {
dispatch_async(self.concurrentQueue, ^{
[self read];
});
dispatch_async(self.concurrentQueue, ^{
[self write];
});
}
// 讀寫操作
- (void)read {
NSLog(@"read---- %d", self.count);
}
- (void)write {
self.count += 1;
NSLog(@"write---- %d", self.count);
}
// 輸出內(nèi)容
2020-07-18 11:47:03.612175+0800 GCD_OC[76121:1709312] read---- 0
2020-07-18 11:47:03.612273+0800 GCD_OC[76121:1709311] read---- 1
2020-07-18 11:47:03.612230+0800 GCD_OC[76121:1709314] write---- 1
2020-07-18 11:47:03.612866+0800 GCD_OC[76121:1709312] write---- 2
2020-07-18 11:47:03.612986+0800 GCD_OC[76121:1709311] write---- 3
2020-07-18 11:47:03.612919+0800 GCD_OC[76121:1709314] read---- 2
2020-07-18 11:47:03.613252+0800 GCD_OC[76121:1709312] read---- 3
2020-07-18 11:47:03.613346+0800 GCD_OC[76121:1709314] write---- 4
2020-07-18 11:47:03.613423+0800 GCD_OC[76121:1709311] read---- 4
每次運(yùn)行的輸出結(jié)果都會(huì)不一樣郎楼,根據(jù)這個(gè)輸出內(nèi)容万伤,我們可以看到在還沒有執(zhí)行到輸出write----1的時(shí)候,就已經(jīng)執(zhí)行了read----1呜袁,在write---- 3之后 read的結(jié)果卻是2敌买。這絕對(duì)是我們所不期望的。其實(shí)在程序設(shè)計(jì)中我們是不應(yīng)該設(shè)計(jì)出多讀多寫這種行為阶界,因?yàn)檫@個(gè)結(jié)果是不可控虹钮。
解決方案之一是對(duì)讀寫操作都加上鎖做成單獨(dú)單寫,這樣是沒問題但有些浪費(fèi)性能膘融,正常寫操作確定之后結(jié)果就確定了芙粱,讀的操作可以多線程同時(shí)進(jìn)行,而不需要等別的線程讀完它才能讀氧映,所以有了多讀單寫的需求春畔。
解決多讀單寫常見有兩種方案,第一種是使用讀寫鎖pthread_rwlock_t
屯耸。
讀寫鎖具有一些幾個(gè)特性:
- 同一時(shí)間拐迁,只能有一個(gè)線程進(jìn)行寫的操作
- 同一時(shí)間,允許有多個(gè)線程進(jìn)行讀的操作疗绣。
- 同一時(shí)間线召,不允許既有寫的操作,又有讀的操作多矮。
這跟我們的多讀單寫需求完美吻合缓淹,也可以說讀寫鎖的設(shè)計(jì)就是為了實(shí)現(xiàn)這一需求的哈打。它的實(shí)現(xiàn)方式如下:
// 執(zhí)行讀寫操作之前需要定義一個(gè)讀寫鎖
@property (nonatomic,assign) pthread_rwlock_t lock;
pthread_rwlock_init(&_lock,NULL);
// 讀寫操作
- (void)read {
pthread_rwlock_rdlock(&_lock);
NSLog(@"read---- %d", self.count);
pthread_rwlock_unlock(&_lock);
}
- (void)write {
pthread_rwlock_wrlock(&_lock);
_count += 1;
NSLog(@"write---- %d", self.count);
pthread_rwlock_unlock(&_lock);
}
// 輸出內(nèi)容
2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722472] read---- 0
2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722471] read---- 0
2020-07-18 12:00:29.364195+0800 GCD_OC[77172:1722469] write---- 1
2020-07-18 12:00:29.364325+0800 GCD_OC[77172:1722472] write---- 2
2020-07-18 12:00:29.364450+0800 GCD_OC[77172:1722470] read---- 2
2020-07-18 12:00:29.364597+0800 GCD_OC[77172:1722471] write---- 3
2020-07-18 12:00:29.366490+0800 GCD_OC[77172:1722469] read---- 3
2020-07-18 12:00:29.366703+0800 GCD_OC[77172:1722472] write---- 4
2020-07-18 12:00:29.366892+0800 GCD_OC[77172:1722489] read---- 4
我們查看輸出日志,所以的讀操作結(jié)果都是最近一次寫操作所賦的值讯壶,這是符合我們預(yù)期的料仗。
還有一種實(shí)現(xiàn)多讀單寫的方案是使用GCD中的柵欄函數(shù)dispatch_barrier
。柵欄函數(shù)的目的就是保證在同一隊(duì)列中它之前的操作全部執(zhí)行完畢再執(zhí)行后面的操作伏蚊。為了保證寫操作的互斥行立轧,我們要對(duì)寫操作執(zhí)行「柵欄」:
// 我們定義一個(gè)用于讀寫的并發(fā)對(duì)列
self.rwQueue = dispatch_queue_create("com.rw.queue", DISPATCH_QUEUE_CONCURRENT);
- (void)read {
dispatch_sync(self.rwQueue, ^{
NSLog(@"read---- %d", self.count);
});
}
- (void)write {
dispatch_barrier_async(self.rwQueue, ^{
self.count += 1;
NSLog(@"write---- %d", self.count);
});
}
這個(gè)輸出結(jié)果跟讀寫鎖實(shí)現(xiàn)是一樣的,也是符合預(yù)期的躏吊。
這里多說幾句氛改,這里的讀和寫分別使用sync
和async
。讀操作要用同步是為了阻塞線程盡快返回結(jié)果比伏,不用擔(dān)心無法實(shí)現(xiàn)多讀胜卤,因?yàn)槲覀兪褂昧瞬l(fā)隊(duì)列,是可以實(shí)現(xiàn)多讀的赁项。至于寫操作使用異步的柵欄函數(shù)葛躏,是為了寫時(shí)不阻塞線程,通過柵欄函數(shù)實(shí)現(xiàn)單寫悠菜。如果我們將讀寫都改成sync或者async舰攒,由于柵欄函數(shù)的機(jī)制是會(huì)順序先讀后寫。如果反過來李剖,讀操作異步芒率,寫操作同步也是可以達(dá)到多讀單寫的目的的囤耳,但讀的時(shí)候不立即返回結(jié)果篙顺,網(wǎng)上有人說只能使用異步方式,防止發(fā)生死鎖充择,這個(gè)說法其實(shí)不對(duì)德玫,因?yàn)橥疥?duì)列是不會(huì)發(fā)生死鎖的。
用GCD如何實(shí)現(xiàn)一個(gè)控制最大并發(fā)數(shù)且執(zhí)行任務(wù)FIFO的功能椎麦?
這個(gè)相對(duì)簡(jiǎn)單宰僧,通過信號(hào)量實(shí)現(xiàn)并發(fā)數(shù)的控制,通過并發(fā)隊(duì)列實(shí)現(xiàn)任務(wù)的FIFO的執(zhí)行
int maxConcurrent = 3;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent);
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// task
dispatch_semaphore_signal(semaphore);
});
作為一個(gè)開發(fā)者观挎,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要琴儿,這是一個(gè)我的iOS開發(fā)交流群:130595548,不管你是大牛還是小白都?xì)g迎入駐 嘁捷,讓我們一起進(jìn)步造成,共同發(fā)展!(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔P巯)