使用多線程是每個(gè)程序員必須要掌握的读串。然而使用多線程的時(shí)候,如果不加注意就會(huì)產(chǎn)生很多比較難排查的bug。所以要對(duì)多線程有深入的理解才行念赶。比如可變數(shù)組和可變字典是線程安全的嗎?本身就是異步執(zhí)行的任務(wù)恰力,如何等待真正的結(jié)果返回之后才繼續(xù)后面的事情叉谜?在線程中睡眠,睡醒之后當(dāng)前類已經(jīng)釋放踩萎,self會(huì)為nil嗎?等等停局,在多線程的實(shí)際使用過(guò)程中會(huì)有很多出現(xiàn),因此需要把基礎(chǔ)打牢香府,也要有更深的理解董栽。
首先從最基本的定義說(shuō)起
什么是進(jìn)程?
進(jìn)程是線程的容器企孩。程序是指令裆泳、數(shù)據(jù)及其組織形式的描述,進(jìn)程是程序的實(shí)體柠硕。
狹義定義:進(jìn)程是正在運(yùn)行的程序的實(shí)例(an instance of a computer program that is being executed)工禾。
上面是百科的部分定義,簡(jiǎn)單來(lái)說(shuō)蝗柔,每個(gè)應(yīng)用啟動(dòng)之后闻葵,都對(duì)應(yīng)著一個(gè)進(jìn)程。打開(kāi)活動(dòng)監(jiān)視器即可看到癣丧,第一列就是進(jìn)程名稱槽畔,每一個(gè)程序?qū)?yīng)一個(gè)進(jìn)程,每個(gè)進(jìn)程都有一個(gè)唯一標(biāo)示PID胁编,即進(jìn)程ID厢钧。
進(jìn)程概念主要有兩點(diǎn):
- 進(jìn)程是一個(gè)實(shí)體。每個(gè)進(jìn)程都有自己的地址空間嬉橙,一般情況下包括 文本區(qū)域text region早直、數(shù)據(jù)區(qū)域data region 和堆棧stack region。
- 進(jìn)程是一個(gè)執(zhí)行中的程序市框。程序是一個(gè)沒(méi)有生命的實(shí)體霞扬,只有程序執(zhí)行的時(shí)候,它才能成為一個(gè)活動(dòng)的實(shí)體。我們稱它為進(jìn)程喻圃。
除去進(jìn)程的新建和終止萤彩,運(yùn)行中的進(jìn)程具有三種基本狀態(tài):
- 就緒:進(jìn)程已經(jīng)獲得除處理器外的所需資源,等待分配處理器資源斧拍。只要分配了處理器進(jìn)程就可以執(zhí)行雀扶。
- 運(yùn)行:進(jìn)行占用處理器資源,出于此狀態(tài)的運(yùn)行數(shù)小于等于處理器數(shù)
- 阻塞:由于進(jìn)程等待某種條件肆汹,比如I/O操作或者進(jìn)程同步愚墓,在條件滿足之前無(wú)法繼續(xù)執(zhí)行
什么是線程?
線程是程序執(zhí)行的最小單元县踢。一個(gè)標(biāo)準(zhǔn)的線程由線程ID转绷,當(dāng)前指令指針,寄存器集合和堆棧組成硼啤。線程是進(jìn)程中的一個(gè)實(shí)體议经。線程也具有就緒,阻塞和運(yùn)行三種基本狀態(tài)谴返。
通常一個(gè)進(jìn)行中可以包含若干個(gè)線程煞肾,它們可以利用進(jìn)程所擁有的全部資源。進(jìn)程是分配資源的基本單位嗓袱。而線程則是獨(dú)立運(yùn)行和調(diào)度的基本單位籍救。由于線程比進(jìn)程更小,基本上不擁有系統(tǒng)資源渠抹,只擁有一點(diǎn)兒在運(yùn)行中必不可少的資源蝙昙,但它可與同屬一個(gè)進(jìn)程的其他線程共享進(jìn)程所擁有的全部資源。
線程是進(jìn)程的基本執(zhí)行單元梧却,進(jìn)程的所有任務(wù)都是在線程中執(zhí)行的奇颠。
多線程的實(shí)現(xiàn)原理
先說(shuō)下任務(wù)執(zhí)行有兩種方式,串行和并行放航。
串行:任務(wù)一個(gè)一個(gè)執(zhí)行烈拒,所需時(shí)間是所有任務(wù)執(zhí)行完成之和。
并行:任務(wù)并發(fā)執(zhí)行广鳍,所需時(shí)間是耗時(shí)最久的任務(wù)完成時(shí)間荆几。
任務(wù)的串行于行與線程并沒(méi)有必然的聯(lián)系。串行并非就只有一個(gè)線程赊时,也可以有多個(gè)線程吨铸。并行必然有多個(gè)線程。
對(duì)于單核操作系統(tǒng)蛋叼,同一時(shí)間只有一個(gè)線程在執(zhí)行焊傅。每個(gè)線程都分配有時(shí)間片進(jìn)行執(zhí)行剂陡,然后切換到其他線程狈涮。從宏觀來(lái)看是并行的狐胎,從微觀上來(lái)看,是串行的歌馍。
對(duì)于多核操作系統(tǒng)握巢,就真正實(shí)現(xiàn)了并行執(zhí)行任務(wù)。在同一時(shí)間可以有多個(gè)線程在執(zhí)行任務(wù)松却。
多線程的場(chǎng)景使用場(chǎng)景:
在iOS系統(tǒng)中,UIKit中的所有操作都是在主線程中執(zhí)行的晓锻,包括用戶的觸摸事件等歌焦。如果在主線程執(zhí)行耗時(shí)操作就會(huì)造成卡頓等現(xiàn)象,影響用戶體驗(yàn)砚哆。常見(jiàn)的耗時(shí)操作有:
網(wǎng)絡(luò)請(qǐng)求
圖片加載
文件處理
數(shù)據(jù)存儲(chǔ)
多任務(wù)
常見(jiàn)實(shí)現(xiàn)方式
- pThread --c 語(yǔ)言
- NSThread -- oc對(duì)象
- GCD -- c語(yǔ)言
- NSOpreation -- oc對(duì)象
pThread 用的是一套c語(yǔ)言庫(kù)独撇,在iOS開(kāi)發(fā)中使用的不多。
使用時(shí)需要先導(dǎo)入頭文件#import <pthread.h>
使用時(shí)創(chuàng)建線程躁锁,并指定執(zhí)行的方法即可
- (IBAction)pThreadClicked:(UIButton *)sender {
NSLog(@"主線程事件");
pthread_t pthread;
pthread_create(&pthread, NULL, run, NULL);
}
void *run(){
NSLog(@"run方法執(zhí)行");
sleep(1);
NSLog(@"執(zhí)行結(jié)束");
return NULL;
}
// 打印結(jié)果4540是進(jìn)程id纷铣,后面的是線程id,打印結(jié)果可以看出run在子線程中執(zhí)行战转。
2019-01-03 17:22:26.260893+0800 ThreadTest[4540:1088694] 主線程事件
2019-01-03 17:22:26.262011+0800 ThreadTest[4540:1089164] run方法執(zhí)行
2019-01-03 17:22:27.263254+0800 ThreadTest[4540:1089164] 執(zhí)行結(jié)束
因?yàn)閜hread不常用搜立,所以也不做多做介紹,使用的時(shí)候時(shí)候看一下官方的api就可以了槐秧。
NSThread 的三種創(chuàng)建方式
? // 1. 對(duì)象方式啄踊,可以獲取到線程對(duì)象,需要手動(dòng)執(zhí)行start方法,當(dāng)然也方便設(shè)置其他屬性刁标,比如優(yōu)先級(jí)颠通,比如線程名字等。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (IBAction)NSTreadClick:(UIButton *)sender {
NSLog(@"主線程");
NSThread *thread = [[NSThread alloc]initWithBlock:^{
sleep(1);
NSLog(@"子線程");
}];
thread.name = @"thread1";
thread.threadPriority = 1.0;
[thread start];
}
// 打印結(jié)果
2019-01-03 17:44:14.421668+0800 ThreadTest[4668:1122856] 主線程
2019-01-03 17:44:15.423002+0800 ThreadTest[4668:1123018] 子線程
? // 2. 類方法 不能直接獲取線程對(duì)象 不過(guò)可以在線程執(zhí)行過(guò)程中使用類方法currentThread獲取當(dāng)前線程
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
// 3. NSObject的分類方法命雀,不能直接獲取線程對(duì)象
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
GCD
以下面的代碼為例
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
...
});
// async 異步
// get_global_queue 并發(fā)
// ^{ ... } 執(zhí)行的任務(wù)
GCD的使用需要告訴gcd三個(gè)東西
- 同步 還是 異步:同步會(huì)阻塞當(dāng)前線程蒜哀,異步不會(huì)阻塞。
- 在哪個(gè)隊(duì)列:隊(duì)列分為串行和并行吏砂,即需要一個(gè)一個(gè)執(zhí)行還是需要并發(fā)執(zhí)行撵儿。
- 任務(wù)是什么: block中的東西即是要執(zhí)行的任務(wù)。
主隊(duì)列:dispatch_get_main_queue() 串行隊(duì)列
全局隊(duì)列: dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>) 并發(fā)隊(duì)列
舉個(gè)異步執(zhí)行任務(wù)回到主線程刷新的例子:
- (IBAction)gcdClick:(UIButton *)sender {
NSLog(@"用戶點(diǎn)擊事件在主線程");
// 在自線程執(zhí)行任務(wù)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"start...");
[NSThread sleepForTimeInterval:3];
NSLog(@"end...");
// 回到主線程 刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回到主線程刷新UI");
});
});
}
// 打印 可以通過(guò)線程id區(qū)分是不是主線程狐血,也可以打印當(dāng)前線程
2019-01-03 19:14:14.841561+0800 ThreadTest[4789:1195593] 用戶點(diǎn)擊事件在主線程
2019-01-03 19:14:14.841752+0800 ThreadTest[4789:1195941] start...
2019-01-03 19:14:17.845246+0800 ThreadTest[4789:1195941] end...
2019-01-03 19:14:17.845676+0800 ThreadTest[4789:1195593] 回到主線程刷新UI
獲取全局隊(duì)列時(shí)淀歇,第一個(gè)參數(shù)是設(shè)置優(yōu)先級(jí)的,第二個(gè)是預(yù)留參數(shù)匈织,暫時(shí)沒(méi)有用浪默。優(yōu)先級(jí)越高牡直,先執(zhí)行的概率越大。
進(jìn)行串行執(zhí)行纳决,串行還是并發(fā)和同步異步?jīng)]有關(guān)系碰逸,也就是和線程沒(méi)有必然的聯(lián)系,我們舉個(gè)例子來(lái)說(shuō)明這一點(diǎn)阔加,同步因?yàn)闆](méi)有開(kāi)啟線程必然是串行的饵史,但異步如果指定了串行隊(duì)列,也會(huì)一個(gè)一個(gè)執(zhí)行胜榔。
同步的串行:
// 同步 -- 串行
NSLog(@"當(dāng)前主線程");
dispatch_queue_t serial = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serial, ^{
NSLog(@"sync-task1");
});
dispatch_sync(serial, ^{
NSLog(@"sync-task2");
});
dispatch_sync(serial, ^{
NSLog(@"sync-task3");
});
異步的串行:
//異步 -- 串行
dispatch_async(serial, ^{
NSLog(@"async-task1");
});
dispatch_async(serial, ^{
NSLog(@"async-task2");
});
dispatch_async(serial, ^{
NSLog(@"async-task3");
});
上述兩者的打印結(jié)果如下:
2019-01-03 19:28:09.275753+0800 ThreadTest[4836:1220108] 當(dāng)前主線程
2019-01-03 19:28:09.276001+0800 ThreadTest[4836:1220108] sync-task1
2019-01-03 19:28:09.276151+0800 ThreadTest[4836:1220108] sync-task2
2019-01-03 19:28:09.276287+0800 ThreadTest[4836:1220108] sync-task3
2019-01-03 19:28:09.276452+0800 ThreadTest[4836:1220151] async-task1
2019-01-03 19:28:09.276656+0800 ThreadTest[4836:1220151] async-task2
2019-01-03 19:28:09.277657+0800 ThreadTest[4836:1220151] async-task3
上述結(jié)果說(shuō)明:
- 同步時(shí)胳喷,沒(méi)有開(kāi)啟線程。
- 異步時(shí)夭织,開(kāi)啟了線程吭露,因?yàn)槭谴校灾婚_(kāi)了一個(gè)線程尊惰。
- 串行是為了保證任務(wù)的順序執(zhí)行讲竿,并行是為了保證任務(wù)的并發(fā)執(zhí)行,串行沒(méi)有創(chuàng)建新線程择浊,并行會(huì)根據(jù)需要至少創(chuàng)建一個(gè)線程戴卜。
關(guān)于同步&異步。串行&并行 可以列個(gè)象限圖
有四種組合:
同步-串行:同步會(huì)阻塞當(dāng)前線程琢岩,因此不會(huì)創(chuàng)建線程投剥,也可以保證任務(wù)同步執(zhí)行
同步-并發(fā):同步會(huì)阻塞當(dāng)前線程,所以不會(huì)創(chuàng)建線程担孔,所以無(wú)法實(shí)現(xiàn)并發(fā)江锨,實(shí)際上還是串行。
異步-串行:異步不會(huì)阻塞當(dāng)前線程糕篇,會(huì)創(chuàng)建新線程啄育,為保證串行,一般創(chuàng)建一個(gè)線程就夠了拌消。
異步-并發(fā):異步不會(huì)阻塞當(dāng)前線程挑豌,會(huì)創(chuàng)建新線程,為保證并發(fā)墩崩,一般會(huì)創(chuàng)建多個(gè)線程氓英。
這里重點(diǎn)說(shuō)明一下同步-并發(fā) 其實(shí)因?yàn)橥綍?huì)阻塞線程所以不能并發(fā)
// 同步-并發(fā),實(shí)際無(wú)法實(shí)現(xiàn)并發(fā)鹦筹,依然是串行執(zhí)行
NSLog(@"當(dāng)前主線程");
dispatch_queue_t async = dispatch_queue_create("com.test.async", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 8; i++) {
dispatch_sync(async, ^{
NSLog(@"sync_concurrent_task%d",i);
});
}
// 打印結(jié)果 并沒(méi)有創(chuàng)建新的線程铝阐,所以并不是并發(fā)執(zhí)行的。
2019-01-03 19:44:25.405840+0800 ThreadTest[4888:1241863] 當(dāng)前主線程
2019-01-03 19:44:25.406140+0800 ThreadTest[4888:1241863] sync_concurrent_task0
2019-01-03 19:44:25.406288+0800 ThreadTest[4888:1241863] sync_concurrent_task1
2019-01-03 19:44:25.406429+0800 ThreadTest[4888:1241863] sync_concurrent_task2
2019-01-03 19:44:25.406534+0800 ThreadTest[4888:1241863] sync_concurrent_task3
2019-01-03 19:44:25.406634+0800 ThreadTest[4888:1241863] sync_concurrent_task4
2019-01-03 19:44:25.406756+0800 ThreadTest[4888:1241863] sync_concurrent_task5
2019-01-03 19:44:25.407134+0800 ThreadTest[4888:1241863] sync_concurrent_task6
2019-01-03 19:44:25.407561+0800 ThreadTest[4888:1241863] sync_concurrent_task7
// 任務(wù)組 — 用來(lái)一組任務(wù)結(jié)束之后铐拐,再執(zhí)行其他操作徘键,組任務(wù)結(jié)束之后會(huì)調(diào)用notify方法
NSLog(@"當(dāng)前主線程");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 1");
});
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 2");
});
dispatch_group_async(group, group_queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group task 3");
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印 啟用了三個(gè)線程 任務(wù)等待完成之后執(zhí)行练对。
2019-01-03 19:55:20.669903+0800 ThreadTest[4924:1260901] 當(dāng)前主線程
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260946] group task 2
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260947] group task 1
2019-01-03 19:55:22.674533+0800 ThreadTest[4924:1260945] group task 3
2019-01-03 19:55:22.674753+0800 ThreadTest[4924:1260946] all task done
需要注意的是:以上每一個(gè)任務(wù)本身就是同步的,如果任務(wù)本身就是異步的吹害,每個(gè)任務(wù)很快就會(huì)執(zhí)行螟凭,可能獲取不到我們想要的結(jié)果。比如我們模擬一個(gè)異步請(qǐng)求赠制。
// 任務(wù)組
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, group_queue, ^{
[self requestOneInfo:^{
NSLog(@"one info done");
}];
});
dispatch_group_async(group, group_queue, ^{
[self requestOtherInfo:^{
NSLog(@"other info done");
}];
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印結(jié)果 很明顯不是我們想要的
2019-01-03 20:07:51.231103+0800 ThreadTest[4950:1281306] 當(dāng)前主線程
2019-01-03 20:07:51.231406+0800 ThreadTest[4950:1281362] get OneInfo start
2019-01-03 20:07:51.231426+0800 ThreadTest[4950:1281360] all task done
2019-01-03 20:07:51.231414+0800 ThreadTest[4950:1281359] get OtherInfo start
2019-01-03 20:07:53.234123+0800 ThreadTest[4950:1281359] get OtherInfo end
2019-01-03 20:07:53.234130+0800 ThreadTest[4950:1281362] get OneInfo end
2019-01-03 20:07:53.234489+0800 ThreadTest[4950:1281359] other info done
2019-01-03 20:07:53.234491+0800 ThreadTest[4950:1281362] one info done
以上結(jié)果很明顯不是我們想要的赂摆,就是因?yàn)槿蝿?wù)本身是異步執(zhí)行的挟憔,任務(wù)組的任務(wù)很快就結(jié)束了钟些,真正的任務(wù)并沒(méi)有結(jié)束。這個(gè)時(shí)候绊谭,我們需要使用 enter 和 leave , enter 和 leave要成對(duì)出現(xiàn)政恍。
我們修改一下代碼,讓任務(wù)組可以執(zhí)行預(yù)期的異步操作
// 任務(wù)組
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t group_queue = dispatch_queue_create("com.test.group", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_group_async(group, group_queue, ^{
[self requestOneInfo:^{
NSLog(@"one info done");
dispatch_group_leave(group);
}];
});
dispatch_group_enter(group);
dispatch_group_async(group, group_queue, ^{
[self requestOtherInfo:^{
NSLog(@"other info done");
dispatch_group_leave(group);
}];
});
dispatch_group_notify(group, group_queue, ^{
NSLog(@"all task done");
});
// 打印达传,符合預(yù)期
2019-01-03 20:13:05.609618+0800 ThreadTest[4963:1290616] 當(dāng)前主線程
2019-01-03 20:13:05.609897+0800 ThreadTest[4963:1290656] get OneInfo start
2019-01-03 20:13:05.609923+0800 ThreadTest[4963:1290654] get OtherInfo start
2019-01-03 20:13:07.615273+0800 ThreadTest[4963:1290656] get OneInfo end
2019-01-03 20:13:07.615284+0800 ThreadTest[4963:1290654] get OtherInfo end
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290654] other info done
2019-01-03 20:13:07.615583+0800 ThreadTest[4963:1290656] one info done
2019-01-03 20:13:07.615914+0800 ThreadTest[4963:1290654] all task done
NSOpreation
是GCD的一種封裝篙耗,需要使用子類。
任務(wù)隊(duì)列:NSOpreationQueue 相當(dāng)于一個(gè)線程池的概念宪赶,可以添加任務(wù)宗弯,設(shè)置最大并發(fā)數(shù)。
任務(wù)有幾種狀態(tài):ready搂妻,canceld蒙保,executing,finished欲主,asynchronous
任務(wù)可以很方便的添加依賴
我們可以使用系統(tǒng)提供了兩個(gè)子類創(chuàng)建NSOpreaion通過(guò)調(diào)用任務(wù)的start方法啟動(dòng)任務(wù)邓厕,會(huì)在當(dāng)前的線程同步執(zhí)行。
如果我們需要異步執(zhí)行扁瓢,通過(guò)創(chuàng)建隊(duì)列详恼,把任務(wù)添加到隊(duì)列即可。
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務(wù)執(zhí)行了");
}];
[op start];// 在當(dāng)前線程同步執(zhí)行
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:op];// 異步執(zhí)行
如果任務(wù)需要設(shè)置依賴關(guān)系引几,調(diào)用任務(wù)的方法
-(void*)addDependency:(NSOperation *)op;
如果我們需要等待所有任務(wù)完成昧互,調(diào)用隊(duì)列的方法
-(void*)waitUntilAllOperationsAreFinished;
注意事項(xiàng):
線程之間共用進(jìn)程所有資源,當(dāng)多線程操作同一個(gè)變量的時(shí)候伟桅,可能會(huì)使得結(jié)果不正確敞掘。
因此要特別注意線程安全的問(wèn)題。
通常保證線程安全有很多種方式
- 使用線程鎖
- 使用串行隊(duì)列
- 使用線程安全的類
- 使用信號(hào)量或runloop使異步看起來(lái)像同步在執(zhí)行
- 注意任務(wù)可能本身就是異步的