你要知道的iOS多線程N(yùn)SThread、GCD叉存、NSOperation、RunLoop都在這里
轉(zhuǎn)載請(qǐng)注明出處 http://www.reibang.com/p/e9d8a087f6c0
本系列文章主要講解iOS中多線程的使用歼捏,包括:NSThread、GCD瓣履、NSOperation以及RunLoop的使用方法詳解,本系列文章不涉及基礎(chǔ)的線程/進(jìn)程袖迎、同步/異步、阻塞/非阻塞燕锥、串行/并行,這些基礎(chǔ)概念归形,有不明白的讀者還請(qǐng)自行查閱鼻由。本系列文章將分以下幾篇文章進(jìn)行講解连霉,讀者可按需查閱嗡靡。
- iOS多線程——你要知道的NSThread都在這里
- iOS多線程——你要知道的GCD都在這里
- iOS多線程——你要知道的NSOperation都在這里
- iOS多線程——你要知道的RunLoop都在這里
- iOS多線程——RunLoop與GCD窟感、AutoreleasePool
GCD的使用姿勢(shì)全解
經(jīng)過前一篇文章的學(xué)習(xí),可以發(fā)現(xiàn)直接使用NSThread
來編寫多線程程序有不少問題柿祈,線程在執(zhí)行完成后就會(huì)退出,每次執(zhí)行任務(wù)都需要?jiǎng)?chuàng)建一個(gè)線程很浪費(fèi)資源躏嚎,其次是需要我們自行進(jìn)行同步操作,自行管理線程的生命周期卢佣,如果要編寫并發(fā)的代碼或者多核的真正并行處理的代碼就比較復(fù)雜了。
本篇文章將會(huì)介紹一個(gè)抽象層次更高的多線程編寫方式GCD
虚茶,GCD
全稱Grand Central Dispatch
是蘋果提供的一個(gè)多核編程的解決方案仇参,在真正意義上實(shí)現(xiàn)了并行操作婆殿,而不是并發(fā)。
GCD
使用線程池
模型來執(zhí)行用戶提交的任務(wù)婆芦,所以它比較節(jié)約資源,不需要為每個(gè)任務(wù)都重新創(chuàng)建一個(gè)新的線程消约,GCD
不需要自行編寫并行代碼,而是自動(dòng)進(jìn)行多核的并行計(jì)算荆陆,自動(dòng)管理線程的生命周期,如:使用線程池管理線程的創(chuàng)建和銷毀被啼,線程的調(diào)度,任務(wù)的調(diào)度等浓体,用戶只需要編寫任務(wù)代碼并提交即可。
GCD
中有兩個(gè)比較重要的概念:任務(wù)和隊(duì)列命浴。
GCD的任務(wù)
任務(wù)顧名思義就是我們需要執(zhí)行的代碼塊,可以是一個(gè)方法也可以是一個(gè)block
媳溺,就是我們需要線程為我們完成的工作,編寫完成的任務(wù)只需提交給GCD
的隊(duì)列悬蔽,即可自動(dòng)幫我們完成任務(wù)的調(diào)度,以及線程的調(diào)度蝎困,可以很方便的以多線程的方式執(zhí)行倍啥。
GCD的隊(duì)列
隊(duì)列用于管理用戶提交的任務(wù)禾乘,GCD
的隊(duì)列有兩種形式虽缕,串行隊(duì)列和并發(fā)隊(duì)列:
- 串行隊(duì)列: GCD底層只維護(hù)一個(gè)線程,任務(wù)只能串行依次執(zhí)行。
- 并發(fā)隊(duì)列: GCD底層使用線程池維護(hù)多個(gè)線程弟塞,任務(wù)可并發(fā)執(zhí)行。
不論是串行隊(duì)列還是并發(fā)隊(duì)列都使用FIFO 先進(jìn)先出
的方式來管理用戶提交的任務(wù)决记。
對(duì)于串行隊(duì)列來說,GCD
每次從串行隊(duì)列的隊(duì)首取一個(gè)任務(wù)交給唯一的一個(gè)線程來處理系宫,直到前一個(gè)任務(wù)完成后建车,才繼續(xù)從隊(duì)列中取下一個(gè)任務(wù)來執(zhí)行扩借,因此缤至,串行隊(duì)列中的任務(wù)執(zhí)行嚴(yán)格按照提交順序,并且后一個(gè)任務(wù)必須等前一個(gè)任務(wù)執(zhí)行完成后才可以執(zhí)行领斥。
對(duì)于并發(fā)隊(duì)列來說,GCD
每次從并發(fā)隊(duì)列的隊(duì)首取一個(gè)任務(wù)月洛,并將這個(gè)任務(wù)按照任務(wù)調(diào)度分發(fā)給多個(gè)線程中的某一個(gè)線程,此時(shí)不需要等待其完成嚼黔,如果隊(duì)列中還有其他任務(wù)繼續(xù)從隊(duì)列中取出并分發(fā)給某一個(gè)線程來執(zhí)行,由于底層由線程池管理多個(gè)線程唬涧,每個(gè)任務(wù)的時(shí)間復(fù)雜度不同再加上線程調(diào)度的影響,后提交的任務(wù)可能先執(zhí)行完成捧搞。但對(duì)于單個(gè)線程來說,只能按順序執(zhí)行,比如某個(gè)線程被安排了多個(gè)任務(wù)陌僵,那這個(gè)線程就只能按提交順序依次執(zhí)行任務(wù)。
所以碗短,我們?cè)谑褂?code>GCD時(shí)也就很簡單了,只需要?jiǎng)?chuàng)建或獲取系統(tǒng)隊(duì)列、編寫任務(wù)并提交任務(wù)到隊(duì)列即可纲堵。首先舉一個(gè)下載圖片的栗子,這個(gè)栗子和第一篇講解NSThread
的栗子一樣席函,但是使用GCD
來實(shí)現(xiàn):
//獲取一個(gè)優(yōu)先級(jí)默認(rèn)的全局并發(fā)隊(duì)列,并以異步的方式提交任務(wù)執(zhí)行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下載圖片
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
//獲取主隊(duì)列茂附,在主線程中更新UI督弓,并以異步方式提交任務(wù)
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
上面的栗子非常簡單营曼,不需要我們手動(dòng)創(chuàng)建線程即可實(shí)現(xiàn)多線程的并發(fā)編程愚隧,如果現(xiàn)在還看不懂沒關(guān)系,學(xué)完本章內(nèi)容你一定會(huì)懂狂塘。
首先看一下GCD
為我們提供了哪些創(chuàng)建隊(duì)列或獲取系統(tǒng)隊(duì)列的方法:
//獲取當(dāng)前執(zhí)行該方法的隊(duì)列,被廢棄了睹耐,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);
/*
獲取主隊(duì)列,即與主線程相關(guān)聯(lián)的隊(duì)列
如果需要提交任務(wù)到主線程使用該方法獲取主線程的主隊(duì)列即可
主隊(duì)列是串行隊(duì)列因?yàn)橹痪S護(hù)主線程一個(gè)線程
*/
dispatch_queue_t dispatch_get_main_queue(void);
/*
獲取一個(gè)全局的并發(fā)隊(duì)列
identifier指定該隊(duì)列的優(yōu)先級(jí)可選值有:
DISPATCH_QUEUE_PRIORITY_HIGH 2
DISPATCH_QUEUE_PRIORITY_DEFAULT 0
DISPATCH_QUEUE_PRIORITY_LOW (-2)
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到傳個(gè)0得了
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
/*
創(chuàng)建一個(gè)隊(duì)列
label 隊(duì)列的名稱
attr 隊(duì)列的屬性可選值有:
DISPATCH_QUEUE_SERIAL 創(chuàng)建一個(gè)串行隊(duì)列
DISPATCH_QUEUE_CONCURRENT 創(chuàng)建一個(gè)并發(fā)隊(duì)列
通過這種方式可以自己維護(hù)一個(gè)隊(duì)列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
具體獲取相關(guān)隊(duì)列的方法如下:
//獲取串行主隊(duì)列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//獲取一個(gè)默認(rèn)優(yōu)先級(jí)的并發(fā)隊(duì)列
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT);
//自定義創(chuàng)建一個(gè)名稱為myConcurrentQueue的并發(fā)隊(duì)列
dispatch_queue_t myConcurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
隊(duì)列創(chuàng)建完成以后就可以編寫任務(wù)并提交了响委,接下來將介紹兩種提交執(zhí)行了,接下來介紹兩種執(zhí)行方式赘风,同步執(zhí)行和異步執(zhí)行。
- 同步執(zhí)行: 阻塞當(dāng)前線程邀窃,直到任務(wù)執(zhí)行完成當(dāng)前線程才可繼續(xù)執(zhí)行
- 異步執(zhí)行: 不阻塞當(dāng)前線程假哎,可能使用其他線程來執(zhí)行任務(wù)瞬捕,不需要等待任務(wù)完成當(dāng)前線程即可立即繼續(xù)執(zhí)行
關(guān)于同步/異步舵抹,阻塞/非阻塞建議看UNIX網(wǎng)絡(luò)編程 卷一
有詳細(xì)的解釋,此處不再贅述了惧蛹。
看一下GCD
提交執(zhí)行任務(wù)的具體方法:
/*
以異步方式執(zhí)行任務(wù)刑枝,不阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)装畅,該block返回值、形參都為void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
/*
同上
context 是一個(gè)void*的指針沧烈,作為work的第一個(gè)形參
work 是一個(gè)函數(shù)指針掠兄,指向返回值為void 形參為void*的函數(shù),且形參不能為NULL掺出,也就是說context一定要傳
使用起來不方便徽千,一般不怎么用,需要使用C函數(shù)汤锨,也可以使用OC方法通過傳遞IMP來執(zhí)行但是會(huì)有編譯警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式執(zhí)行任務(wù),阻塞當(dāng)前線程闲礼,必須等待任務(wù)完成當(dāng)前線程才可繼續(xù)執(zhí)行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任務(wù)牍汹,并重復(fù)執(zhí)行iterations次
iterations 迭代執(zhí)行次數(shù)
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)柬泽,該block返回值為void形參為iterations迭代次數(shù)
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue, DISPATCH_NOESCAPE void (^block)(size_t));
//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));
/*
以異步方式提交任務(wù)慎菲,在when時(shí)間點(diǎn)提交任務(wù)
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)锨并,該block返回值露该、形參都為void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以異步方式提交任務(wù),會(huì)阻塞queue隊(duì)列第煮,但不阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列解幼,任務(wù)最終交由該隊(duì)列來執(zhí)行
需要說明的是,即時(shí)使用并發(fā)隊(duì)列包警,該隊(duì)列也會(huì)被阻塞撵摆,前一個(gè)任務(wù)執(zhí)行完成才能執(zhí)行下一個(gè)任務(wù)
block block形式的任務(wù),該block返回值害晦、形參都為void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
以同步方式提交任務(wù)特铝,會(huì)阻塞queue隊(duì)列,也會(huì)阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列壹瘟,任務(wù)最終交由該隊(duì)列來執(zhí)行
同樣的鲫剿,即時(shí)是并發(fā)隊(duì)列該隊(duì)列也會(huì)被阻塞,需要等待前一個(gè)任務(wù)完成稻轨,同時(shí)線程也會(huì)阻塞
block block形式的任務(wù)灵莲,該block返回值、形參都為void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);
/*
底層線程池控制block任務(wù)在整個(gè)應(yīng)用的生命周期內(nèi)只執(zhí)行一次
predicate 實(shí)際為long類型澄者,用于判斷是否執(zhí)行過
block block形式的任務(wù)笆呆,該block返回值、形參都為void
該方法常用于實(shí)現(xiàn)單例類粱挡,以及結(jié)合RunLoop創(chuàng)建一個(gè)常駐內(nèi)存的線程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);
猛的一看常用的方法就有十二種呢赠幕,但是可以發(fā)現(xiàn)每一類方法都提供了block
任務(wù)和function
任務(wù)兩種形式,所以常用的也就七種询筏,只是對(duì)應(yīng)了block
版本和函數(shù)版本榕堰。接下來先介紹最常用的同步執(zhí)行和異步執(zhí)行,其他的方法后文會(huì)講嫌套。
單拎出來同步/異步很好理解逆屡,但是結(jié)合了串行隊(duì)列和并發(fā)隊(duì)列以后情況就有點(diǎn)復(fù)雜了,同步/異步執(zhí)行和串行/并發(fā)隊(duì)列兩兩組合就有四種組合方式踱讨,接下來我們一一查看相關(guān)栗子:
- (void)viewWillAppear:(BOOL)animated
{
//手動(dòng)創(chuàng)建了一個(gè)并發(fā)隊(duì)列
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//也可以獲取全局的并發(fā)隊(duì)列魏蔗,效果一樣
//dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//異步提交一個(gè)任務(wù)到異步隊(duì)列
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
//異步提交一個(gè)任務(wù)到異步隊(duì)列
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
//異步提交一個(gè)任務(wù)到異步隊(duì)列
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
//使用傳遞函數(shù)指針的方式有點(diǎn)復(fù)雜,以后的栗子不再贅述
int context = 0;
dispatch_async_f(concurrentQueue, &context, cFuncTask);
//也可以使用OC方法痹筛,傳入IMP莺治,但會(huì)有警告
//dispatch_async_f(concurrentQueue, &context, [self methodForSelector:@selector(ocFuncTask:)]);
}
//該函數(shù)是C函數(shù)
void cFuncTask(void* context)
{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
}
}
//OC方法
- (void)ocFuncTask:(void*) context
{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
}
}
上述代碼輸出的東西比較多,現(xiàn)在的手機(jī)CPU都很強(qiáng)大帚稠,可能一個(gè)時(shí)間片就一次性處理完了谣旁,就看不到并發(fā)的輸出結(jié)果了,所以這里輸出的次數(shù)比較多滋早,最終的結(jié)果就是Task1-4
亂序輸出榄审,摘取四個(gè)輸出結(jié)果如下:
Task4 <NSThread: 0x1c04776c0>{number = 7, name = (null)} 88
Task3 <NSThread: 0x1c04770c0>{number = 6, name = (null)} 63
Task1 <NSThread: 0x1c0474980>{number = 4, name = (null)} 99
Task2 <NSThread: 0x1c427c5c0>{number = 5, name = (null)} 0
可以發(fā)現(xiàn)每一個(gè)任務(wù)都是用了不同的線程來執(zhí)行,所以通過異步提交任務(wù)到一個(gè)并發(fā)隊(duì)列是真正實(shí)現(xiàn)了并發(fā)執(zhí)行杆麸。
接下來看一下異步提交到一個(gè)串行隊(duì)列搁进,栗子如下:
- (void)viewWillAppear:(BOOL)animated
{
//創(chuàng)建一個(gè)串行隊(duì)列
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
//這里也可以用主隊(duì)列,因?yàn)橹麝?duì)列也是串行隊(duì)列
//dispatch_queue_t serialQueue = dispatch_get_main_queue();
//異步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_async(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
//異步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_async(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
//異步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_async(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
}
執(zhí)行程序后可以發(fā)現(xiàn)角溃,不論輸出再多次都是按照Task1-3
順序輸出拷获,也就是后一個(gè)任務(wù)必須在前一個(gè)任務(wù)完成后才能執(zhí)行,摘取三個(gè)輸出結(jié)果如下:
Task1 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task2 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task3 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
通過結(jié)果不難發(fā)現(xiàn)三個(gè)任務(wù)使用的是同一個(gè)線程减细,因?yàn)榇嘘?duì)列的底層只維護(hù)一個(gè)線程匆瓜,所以三個(gè)任務(wù)只能使用同一個(gè)線程來執(zhí)行,而且單個(gè)線程的執(zhí)行是串行的未蝌,所以才會(huì)造成上述輸出結(jié)果驮吱,這里使用異步執(zhí)行沒有阻塞當(dāng)前線程。
異步執(zhí)行的栗子實(shí)驗(yàn)完了萧吠,可以發(fā)現(xiàn)左冬,異步執(zhí)行僅僅不會(huì)阻塞當(dāng)前線程,但是否是并發(fā)執(zhí)行需要依靠傳入的隊(duì)列纸型,如果傳遞的是串行隊(duì)列就是串行執(zhí)行拇砰,傳入的是并發(fā)隊(duì)列就是并發(fā)執(zhí)行梅忌,接下來看一下同步執(zhí)行的實(shí)驗(yàn)。
- (void)viewWillAppear:(BOOL)animated
{
/*
創(chuàng)建一個(gè)串行隊(duì)列
這里不可以使用主隊(duì)列了除破,因?yàn)閳?zhí)行該方法的是主線程牧氮,如果使用同步執(zhí)行提交到主隊(duì)列會(huì)造成死鎖,后文會(huì)有具體講解
*/
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
//同步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
//同步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
//同步提交一個(gè)任務(wù)到串行隊(duì)列
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
}
執(zhí)行程序后可以發(fā)現(xiàn)瑰枫,不論輸出再多次都是按照Task1-3
順序輸出踱葛,也就是后一個(gè)任務(wù)必須在前一個(gè)任務(wù)完成后才能執(zhí)行,但這里的順序執(zhí)行和前一個(gè)異步提交到串行隊(duì)列不同光坝,異步提交不會(huì)造成線程阻塞尸诽,所以三個(gè)任務(wù)都被提交到了串行隊(duì)列中,但是由于線程的執(zhí)行是按順序的盯另,所以三個(gè)任務(wù)按次序依次執(zhí)行性含。而這里是使用同步提交到串行隊(duì)列去執(zhí)行任務(wù),當(dāng)?shù)谝粋€(gè)dispatch_sync
方法執(zhí)行后會(huì)阻塞當(dāng)前線程鸳惯,必須得等第一個(gè)任務(wù)完成后才能繼續(xù)胶滋,所以這里的執(zhí)行順序是提交第一個(gè)任務(wù)后就開始執(zhí)行而且得等到第一個(gè)任務(wù)完成后再去執(zhí)行第二個(gè)dispatch_sync
方法用于提交第二個(gè)任務(wù),以此類推悲敷。雖然結(jié)果是一致的究恤,但執(zhí)行順序是有差別的,需要注意后德,摘取三個(gè)輸出結(jié)果如下:
Task1 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
可以發(fā)現(xiàn)三個(gè)任務(wù)使用了同一個(gè)線程來執(zhí)行部宿,但是這個(gè)線程有點(diǎn)特殊,它是主線程瓢湃,由于viewWillAppear:
方法是在主線程中執(zhí)行的理张,所以這里也就直接使用了主線程。
接下來進(jìn)行最后一個(gè)實(shí)驗(yàn)绵患,同步提交到并發(fā)隊(duì)列執(zhí)行:
- (void)viewWillAppear:(BOOL)animated
{
//手動(dòng)創(chuàng)建了一個(gè)并發(fā)隊(duì)列
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//也可以獲取全局的并發(fā)隊(duì)列茬射,效果一樣
//dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
//同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
//同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
dispatch_sync(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
}
執(zhí)行程序后可以發(fā)現(xiàn)碑诉,不論輸出多少次都是按Task1-3
順序輸出,相信大家應(yīng)該明白是為什么了,因?yàn)橥教峤蛔枞?dāng)前線程屹徘,第一個(gè)dispatch_sync
提交的任務(wù)完成以后當(dāng)前線程才能去執(zhí)行第二個(gè)dispatch_sync
方法然后執(zhí)行第二個(gè)任務(wù)糊肤。所以资昧,即時(shí)是并發(fā)隊(duì)列居夹,采用同步提交也沒什么卵用了。摘取三個(gè)輸出如下:
Task1 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0263200>{number = 1, name = main} 0
從上面的輸出可以看出管行,三個(gè)任務(wù)都是用主線程來執(zhí)行厨埋,按照并發(fā)隊(duì)列的特性,這里的三個(gè)任務(wù)完全可能由不同的三個(gè)線程來執(zhí)行捐顷,但由于viewWillAppear:
方法是主線程執(zhí)行的荡陷,而且主線程又被阻塞了雨效,底層可能因此選擇了主線程來執(zhí)行,多運(yùn)行幾次就會(huì)發(fā)現(xiàn)也有可能使用其他線程來執(zhí)行废赞。
到此為止设易,四個(gè)實(shí)驗(yàn)都結(jié)束了,沒有單獨(dú)把主隊(duì)列拿出來做實(shí)驗(yàn)蛹头,因?yàn)橹麝?duì)列本質(zhì)還是一個(gè)串行隊(duì)列,其實(shí)驗(yàn)結(jié)果和串行隊(duì)列是一樣的戏溺。通過四組實(shí)驗(yàn)不難發(fā)現(xiàn)渣蜗,想要實(shí)現(xiàn)并發(fā)只能通過異步提交到并發(fā)隊(duì)列來執(zhí)行任務(wù),實(shí)驗(yàn)分析如下表:
type | Serial串行隊(duì)列 | Concurrent并發(fā)隊(duì)列 |
---|---|---|
async異步執(zhí)行 | 不阻塞當(dāng)前線程旷祸,使用其他線程串行執(zhí)行任務(wù)耕拷,只有一個(gè)線程用于執(zhí)行任務(wù) | 不阻塞當(dāng)前線程,并發(fā)執(zhí)行任務(wù)托享,使用多個(gè)線程執(zhí)行任務(wù) |
sync同步執(zhí)行 | 阻塞當(dāng)前線程骚烧,使用同一線程串行執(zhí)行任務(wù),只有一個(gè)線程用于執(zhí)行任務(wù) | 阻塞當(dāng)前線程闰围,可能使用同一線程串行執(zhí)行任務(wù) |
所以赃绊,針對(duì)異步執(zhí)行/同步執(zhí)行和串行隊(duì)列/并發(fā)隊(duì)列,只需要掌握其關(guān)鍵就可以了羡榴,同步/異步的區(qū)別在于是否阻塞線程碧查,串行/并發(fā)隊(duì)列的區(qū)別在于有多少個(gè)線程參與執(zhí)行任務(wù)。即時(shí)存在嵌套結(jié)構(gòu)也能夠很好理解了校仑。舉一個(gè)嵌套結(jié)構(gòu)的例子:
- (void)viewWillAppear:(BOOL)animated
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
for (int i = 0; i < 100; i++)
{
NSLog(@"Complete.");
}
});
}
外層dispatch不論使用串行隊(duì)列還是并發(fā)隊(duì)列忠售,由于只有一個(gè)任務(wù),只會(huì)有一個(gè)線程來執(zhí)行這個(gè)block塊的內(nèi)容迄沫,而同步和異步的區(qū)別就在于是否會(huì)阻塞當(dāng)前線程稻扬,接下來看block塊的內(nèi)容,采用了三個(gè)異步提交到并發(fā)隊(duì)列羊瘩,所以并發(fā)隊(duì)列里就有了三個(gè)不同的任務(wù)泰佳,就可以真正執(zhí)行并發(fā),由于都是異步提交沒有阻塞當(dāng)前線程尘吗,所以輸出Complete
的代碼也會(huì)摻雜在Task1-3
中亂序輸出乐纸。
dispatch_apply
通過上面的講解就已經(jīng)很清楚的了解了GCD
同步/異步提交到串行/并發(fā)隊(duì)列的執(zhí)行過程了。接下來再繼續(xù)講解幾個(gè)常用的方法摇予,再舉個(gè)栗子:
- (void)viewWillAppear:(BOOL)animated
{
//執(zhí)行該方法的是主線程汽绢,不能傳入主隊(duì)列否則會(huì)死鎖,后文會(huì)講解
dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
NSLog(@"Task %@ %ld", [NSThread currentThread], t);
});
}
dispatch_apply
方法是以同步方式提交執(zhí)行任務(wù)侧戴,這里傳入了一個(gè)全局的并發(fā)隊(duì)列宁昭,因此講道理重復(fù)執(zhí)行任務(wù)時(shí)就應(yīng)該有多個(gè)線程并發(fā)執(zhí)行跌宛,但是不管我迭代多少次運(yùn)行多少次都只有一個(gè)輸出是其他線程輸出的,剩余的都是同一個(gè)線程輸出积仗,有懂的讀者可以留言講解一下疆拘。如果傳入的是串行隊(duì)列,那么迭代就是按照順序依次執(zhí)行寂曹。
dispatch_after
再看一個(gè)栗子:
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"Before");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"In %@", [NSThread currentThread]);
});
NSLog(@"After");
}
dispatch_after
是在when時(shí)間點(diǎn)異步提交任務(wù)哎迄,所以不會(huì)阻塞當(dāng)前線程,這里設(shè)置的時(shí)間點(diǎn)是當(dāng)前時(shí)間的5s后隆圆,觀察輸出:
2017-10-19 17:44:19.274305+0800 StudyOCTest[25385:13635522] Before
2017-10-19 17:44:19.274365+0800 StudyOCTest[25385:13635522] After
2017-10-19 17:44:24.745273+0800 StudyOCTest[25385:13635542] In <NSThread: 0x1c027ae40>{number = 3, name = (null)}
可以看出這個(gè)5s并不是精確的5s漱挚,因?yàn)樵摲椒ㄊ窃?code>when時(shí)間點(diǎn)到達(dá)的時(shí)候去提交任務(wù)到隊(duì)列,所以是延遲提交渺氧,而不是延遲執(zhí)行旨涝,隊(duì)列什么時(shí)候安排線程去執(zhí)行是未知的,所以不要用這個(gè)方法去實(shí)現(xiàn)定時(shí)器這樣的功能侣背。
dispatch_barrier _ (a)sync
該方法用于阻塞隊(duì)列白华,舉個(gè)栗子如下:
- (void)viewWillAppear:(BOOL)animated
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task0 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_barrier_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
dispatch_async(concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
}
});
}
上面的輸出是按照Task0 Task1
并發(fā)執(zhí)行,Task2
等待Task0 Task1
執(zhí)行完成后單獨(dú)執(zhí)行贩耐, 最后Task3 Task4
等待Task2
執(zhí)行完成后開始并發(fā)執(zhí)行 弧腥。
這里需要講解一下阻塞隊(duì)列的概念,前文講過不論是并發(fā)隊(duì)列還是串行隊(duì)列都是使用FIFO 先進(jìn)先出
的方式管理的潮太,隊(duì)列會(huì)從隊(duì)首獲取要執(zhí)行的任務(wù)并交由對(duì)應(yīng)線程處理鸟赫,串行隊(duì)列只有一個(gè)線程所以是順序執(zhí)行,并發(fā)隊(duì)列有多個(gè)線程消别,但獲取任務(wù)依舊是FIFO
按順序獲取抛蚤,只是執(zhí)行時(shí)有多個(gè)線程。阻塞線程即寻狂,獲取一個(gè)任務(wù)后岁经,這個(gè)任務(wù)必須要執(zhí)行完成才能獲取下一個(gè)任務(wù),所以不管是并發(fā)還是串行隊(duì)列蛇券,都得等前一個(gè)任務(wù)完成了才能從隊(duì)列中獲取下一個(gè)任務(wù)缀壤,這樣就不難理解輸出結(jié)果了,上述栗子改成串行隊(duì)列結(jié)果也是一樣的纠亚,如果使用同步提交效果也是一樣的塘慕,讀者可以自行嘗試,篇幅問題不再贅述了蒂胞。
dispatch_barrier_async
方法常與并發(fā)隊(duì)列共用图呢,前一段任務(wù)使用dispatch_async
異步并發(fā)執(zhí)行,然后插入一個(gè)dispatch_barrier_async
執(zhí)行一個(gè)中間任務(wù),這個(gè)中間任務(wù)必須要等待前面的并發(fā)任務(wù)執(zhí)行完成后才能開始執(zhí)行蛤织,接著這個(gè)中間任務(wù)完成后赴叹,繼續(xù)異步并發(fā)執(zhí)行接下來的任務(wù)。
dispatch_once
該方法能夠保證在應(yīng)用的生命周期內(nèi)只執(zhí)行一次提交的任務(wù)指蚜,所以常用于單例類的創(chuàng)建乞巧,舉個(gè)單例類的栗子如下:
@interface MyUtil: NSObject <NSCopying>
+ (instancetype)sharedUtil;
@end
@implementation MyUtil
static MyUtil *staticMyUtil = nil;
+ (instancetype)sharedUtil
{
//保證初始化創(chuàng)建只執(zhí)行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticMyUtil = [[MyUtil alloc] init];
});
return staticMyUtil;
}
//防止通過alloc或new直接創(chuàng)建對(duì)象
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
//保證alloc函數(shù)只執(zhí)行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticMyUtil = [super allocWithZone:zone];
});
return staticMyUtil;
}
//實(shí)現(xiàn)NSCopying協(xié)議的方法,防止通過copy獲取副本對(duì)象
- (instancetype)copyWithZone:(NSZone *)zone
{
return staticMyUtil;
}
@end
dispatch_once
函數(shù)需要傳入一個(gè)long
類型的predicate摊鸡,這個(gè)值必須是獨(dú)一無二的绽媒,使用靜態(tài)變量的地址最合適不過了,MyUtil
實(shí)現(xiàn)了NSCopying
協(xié)議的copyWithZone:
方法免猾,防止通過copy
方法獲取副本對(duì)象是辕。
當(dāng)使用alloc&&init
方法初始化時(shí),先調(diào)用allocWithZone:
方法來分配存儲(chǔ)空間掸刊,如果再次使用sharedUtil
方法來獲取的話,由于沒有執(zhí)行過赢乓,會(huì)執(zhí)行到dispatch_once
內(nèi)部block忧侧,此時(shí)會(huì)再去執(zhí)行allocWithZone:
方法,但該方法內(nèi)部dispatch_once
已經(jīng)執(zhí)行過了會(huì)直接返回staticMyUtil
牌芋,反過來調(diào)用是一樣的道理蚓炬,通過這樣的方式就可以實(shí)現(xiàn)真正的單例了。
dispatch_ group_ t
dispatch_group_t
是一個(gè)比較實(shí)用的方法躺屁,通過構(gòu)造一個(gè)組的形式肯夏,將各個(gè)同步或異步提交任務(wù)都加入到同一個(gè)組中,當(dāng)所有任務(wù)都完成后會(huì)收到通知犀暑,用于進(jìn)一步處理驯击,通過這樣的方式就可以實(shí)現(xiàn)多線程下載,當(dāng)下載完成后就可以通知用戶了耐亏,舉個(gè)簡單的栗子如下:
- (void)viewWillAppear:(BOOL)animated
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_async(group, concurrentQueue, ^{
for (int i = 0; i < 500; i++)
{
NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
}
});
dispatch_group_notify(group, concurrentQueue, ^{
NSLog(@"All Task Complete");
});
}
像一個(gè)組中添加了三個(gè)異步的任務(wù)徊都,最終三個(gè)任務(wù)完成后可以收到通知執(zhí)行回調(diào)的block,上面的輸出為广辰,All Task Complete
在前面三個(gè)輸出都結(jié)束后才會(huì)輸出暇矫。
防止GCD產(chǎn)生死鎖
接下來將講解一下GCD
使用時(shí)可能會(huì)產(chǎn)生死鎖的情況,首先舉一個(gè)比較簡單的栗子:
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"Before");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"In");
});
NSLog(@"After");
}
上述代碼就會(huì)產(chǎn)生死鎖择吊,分析下原因李根,首先,viewWillAppear:
方法是在主線程中執(zhí)行的几睛,接著調(diào)用dispatch_sync
方法房轿,該方法會(huì)阻塞當(dāng)前線程,也就是會(huì)阻塞主線程,主線程被阻塞是為了等待任務(wù)的完成冀续,然后該代碼將任務(wù)添加到了主隊(duì)列琼讽,主隊(duì)列會(huì)將任務(wù)交給主線程執(zhí)行,但此時(shí)主線程阻塞了洪唐,任務(wù)添加進(jìn)了主線程得不到運(yùn)行钻蹬,而主線程在等待任務(wù)的執(zhí)行,因此就造成了死鎖凭需。
這個(gè)栗子一般人寫不出來這樣的代碼问欠,僅僅是為了講解什么情況下會(huì)造成死鎖,即粒蜈,線程被阻塞需要等到任務(wù)執(zhí)行完成顺献,而任務(wù)由于線程阻塞得不到執(zhí)行。前文舉的幾個(gè)串行隊(duì)列的栗子很多是不能使用主隊(duì)列的枯怖,原因也正在此注整。
再舉個(gè)栗子:
dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
NSLog(@"===== %@ %ld", [NSThread currentThread], t);
});
});
上述栗子也會(huì)造成死鎖,因?yàn)椋?code>dispatch_apply同樣會(huì)阻塞當(dāng)前線程度硝,它需要等待內(nèi)部的dispatch_apply
執(zhí)行完成肿轨,內(nèi)部的需要等待外部的線程來執(zhí)行它,產(chǎn)生了死鎖蕊程。
可以看出死鎖產(chǎn)生的條件一般都發(fā)生在同步執(zhí)行方法中椒袍,所以,在使用同步執(zhí)行方法時(shí)要避免任務(wù)再次派發(fā)到同一個(gè)線程中藻茂。
實(shí)現(xiàn)定時(shí)器的三種方法
定時(shí)器在開發(fā)中是比較常見的需求驹暑,常用的其實(shí)有三種方法:NSTimer
、GCD
以及CADisplayLink
辨赐,CADisplayLink
是其中精度最高的优俘,因?yàn)樗噲D與屏幕刷新率保持一致,由于涉及的內(nèi)容比較多本小結(jié)只介紹基本使用方法掀序,直接看栗子:
- (void)viewWillAppear:(BOOL)animated
{
//倒計(jì)時(shí)次數(shù)
__block int count = 10;
//間隔1s執(zhí)行一次
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
//如果還在倒計(jì)時(shí)次數(shù)內(nèi)
if (count > 0)
{
//執(zhí)行相關(guān)工作兼吓,如果有UI更新的操作需要放到主線程
dispatch_async(dispatch_get_main_queue(), ^{
});
//次數(shù)--
count --;
}
else
{
//次數(shù)到達(dá),取消定時(shí)器
[timer invalidate];
}
}];
//加入到RunLoop中森枪,使用NSRunLoopCommonModes在滑動(dòng)時(shí)也可以繼續(xù)執(zhí)行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
上面的栗子比較簡單视搏,使用了block
的形式實(shí)現(xiàn),也可以使用方法的形式執(zhí)行:
- (void)viewWillAppear:(BOOL)animated
{
self.count = 10;
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(countDown:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)countDown:(NSTimer*)timer
{
if (self.count > 0)
{
//執(zhí)行相關(guān)工作县袱,如果有UI更新的操作需要放到主線程
dispatch_async(dispatch_get_main_queue(), ^{
});
self.count --;
}
else
{
//取消定時(shí)器
[timer invalidate];
}
}
以上比較重要的就是引用循環(huán)
的問題浑娜,創(chuàng)建NSTimer
傳入的target
對(duì)象,NSTimer
會(huì)持有強(qiáng)引用式散,所以在重復(fù)執(zhí)行NSTimer
時(shí)一定要在任務(wù)結(jié)束后調(diào)用invalidate
方法取消定時(shí)器打破引用循環(huán)筋遭,如果只執(zhí)行一次可以不需要。
接下來看一下GCD
如何實(shí)現(xiàn):
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
//執(zhí)行次數(shù)
__block int count = 10;
//獲取一個(gè)全局并發(fā)隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//這里不能使用局部變量,因?yàn)楫?dāng)viewDidAppear函數(shù)返回后timer就會(huì)被釋放
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置timer的執(zhí)行時(shí)間和間隔時(shí)間漓滔,設(shè)置每秒執(zhí)行一次
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 0);
//設(shè)置timer執(zhí)行事件的block塊
dispatch_source_set_event_handler(_timer, ^{
if (count > 0)
{
//要執(zhí)行的任務(wù)编饺,更新UI需要放到主線程
dispatch_async(dispatch_get_main_queue(), ^{
});
count --;
}
else
{
//執(zhí)行次數(shù)達(dá)到預(yù)期就取消timer
dispatch_source_cancel(_timer);
}
});
//啟動(dòng)timer
dispatch_resume(_timer);
}
上面代碼比較簡單,也可以傳入一個(gè)C函數(shù)而不使用塊响驴,具體不再贅述了透且,有興趣的讀者可以自行實(shí)驗(yàn)。
最后看一下CADisplayLink
的栗子:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
self.count = 10;
//CADisplayLink只有這一個(gè)構(gòu)造方法
CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown:)];
//每秒對(duì)多少幀感興趣豁鲤,也就是每秒要執(zhí)行多少次回調(diào)方法
timer.preferredFramesPerSecond = 1;
//必須要添加進(jìn)RunLoop才開始執(zhí)行
[timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)countDown:(CADisplayLink*)timer
{
if (self.count > 0)
{
//執(zhí)行相關(guān)工作秽誊,如果有UI更新的操作需要放到主線程
dispatch_async(dispatch_get_main_queue(), ^{
});
self.count --;
}
else
{
//取消定時(shí)器
[timer invalidate];
}
}
上面栗子不再贅述了,具體細(xì)節(jié)可以自行查閱琳骡。
備注
由于作者水平有限锅论,難免出現(xiàn)紕漏,如有問題還請(qǐng)不吝賜教楣号。