前言
GCD是蘋(píng)果為多核的并行運(yùn)算提出的解決方案,所以會(huì)自動(dòng)合理地利用更多的CPU內(nèi)核(比如雙核、四核),最重要的是它會(huì)自動(dòng)管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷(xiāo)毀線程),完全不需要我們管理,我們只需要告訴干什么就行。同時(shí)GCD抽象層次最高,當(dāng)然是用起來(lái)也最簡(jiǎn)單,只是它基于C語(yǔ)言開(kāi)發(fā),并不像NSOperation是面向?qū)ο蟮拈_(kāi)發(fā),而是完全面向過(guò)程的。
GCD是一種輕量的基于block的線程模型,底層實(shí)現(xiàn)主要有Dispatch Queue和Dispatch Source
Dispatch Queue :管理block(操作)
Dispatch Source :處理事件
Dispatch Queue&Dispatch Source更多相關(guān)知識(shí)
GCD推出來(lái)以后,開(kāi)發(fā)者可以不直接操縱線程,而是將所要執(zhí)行的任務(wù)封裝成一個(gè)unit丟給線程池去處理,線程池會(huì)有效管理線程的并發(fā),控制線程的生死。因此,現(xiàn)在如果考慮到并發(fā)場(chǎng)景,基本上是圍繞著GCD和NSOperationQueue來(lái)展開(kāi)討論。
任務(wù)和隊(duì)列
這里就不得不提到兩個(gè)概念:任務(wù)和隊(duì)列
- 任務(wù):即操作,就是一段代碼,在 GCD 中就是一個(gè) Block,所以添加任務(wù)十分方便。調(diào)度隊(duì)列執(zhí)行任務(wù)有兩種方式: 同步執(zhí)行 和 異步執(zhí)行.
同步派發(fā)(sync) 和 異步派發(fā)(async) 的主要區(qū)別在于會(huì)不會(huì)阻塞當(dāng)前線程,直到 Block 中的任務(wù)執(zhí)行完畢!
【異步并不一定會(huì)開(kāi)啟多線程,當(dāng)在主線程中派發(fā)任務(wù)到主隊(duì)列后,會(huì)等待主線程空閑時(shí)才會(huì)調(diào)度該任務(wù)并沒(méi)有開(kāi)啟新的線程;添加到其他線程時(shí),會(huì)開(kāi)啟新的線程調(diào)度任務(wù)。】
如果是 同步(sync) 操作,它會(huì)阻塞當(dāng)前線程并等待 Block 中的任務(wù)執(zhí)行完畢,然后當(dāng)前線程才會(huì)繼續(xù)往下運(yùn)行。
如果是 異步(async)操作,當(dāng)前線程會(huì)直接往下執(zhí)行,它不會(huì)阻塞當(dāng)前線程。
- 隊(duì)列:調(diào)度隊(duì)列是一個(gè)類(lèi)似對(duì)象的結(jié)構(gòu)體,它管理您提交給它的任務(wù)。所有的調(diào)度隊(duì)列都是先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)。隊(duì)列和線程的區(qū)別,他們之間并沒(méi)有“擁有關(guān)系(ownership)”。隊(duì)列用于存放任務(wù)。一共有兩種隊(duì)列, 串行隊(duì)列 和 并行隊(duì)列。
放到串行隊(duì)列的任務(wù),GCD 會(huì) FIFO(先進(jìn)先出) 地取出來(lái)一個(gè),執(zhí)行一個(gè),然后取下一個(gè),這樣一個(gè)一個(gè)的執(zhí)行。
放到并行隊(duì)列的任務(wù),GCD 也會(huì) FIFO的取出來(lái),但不同的是,它取出來(lái)一個(gè)就會(huì)放到別的線程,然后再取出來(lái)一個(gè)又放到另一個(gè)的線程。這樣由于取的動(dòng)作很快,忽略不計(jì),看起來(lái),所有的任務(wù)都是一起執(zhí)行的。不過(guò)需要注意,GCD 會(huì)根據(jù)系統(tǒng)資源控制并行的數(shù)量,所以如果任務(wù)很多,它并不會(huì)讓所有任務(wù)同時(shí)執(zhí)行。
GCD中不同隊(duì)列中不同任務(wù)的執(zhí)行情況如下表:
同步執(zhí)行的任務(wù) | 異步執(zhí)行的任務(wù) | |
---|---|---|
串行隊(duì)列中 | 當(dāng)前線程,一個(gè)一個(gè)執(zhí)行 | 其他線程,一個(gè)一個(gè)執(zhí)行 |
并行隊(duì)列中 | 當(dāng)前線程,一個(gè)一個(gè)執(zhí)行 | 同時(shí)開(kāi)很多線程,一起執(zhí)行 |
隊(duì)列的創(chuàng)建和執(zhí)行
#開(kāi)辟隊(duì)列的方法:
dispatch_queue_t myQueue = dispatch_queue_create("MyQueue", NULL);
參數(shù)1:標(biāo)簽,用于區(qū)分隊(duì)列
參數(shù)2:隊(duì)列的類(lèi)型,表示這個(gè)隊(duì)列是串行隊(duì)列還是并發(fā)隊(duì)列NUll表示串行隊(duì)列,
DISPATCH_QUEUE_SERIAL 或 NULL 表示創(chuàng)建串行隊(duì)列。
DISPATCH_QUEUE_CONCURRENT 表示創(chuàng)建并行隊(duì)列。
#執(zhí)行隊(duì)列的方法
異步執(zhí)行
dispatch_async(<#dispatch_queue_t queue#>, <#^(void)block#>)
同步執(zhí)行
dispatch_sync(<#dispatch_queue_t queue#>, <#^(void)block#>)
幾個(gè)特別的隊(duì)列
- 主隊(duì)列(Main Queue):專門(mén)負(fù)責(zé)調(diào)度主線程(Main Thread)的任務(wù),是一個(gè)串行隊(duì)列,沒(méi)有辦法開(kāi)辟新的線程。任何需要刷新 UI 的工作都要在主隊(duì)列執(zhí)行,所以一般耗時(shí)的任務(wù)都要放到別的線程執(zhí)行。
這里需要特別說(shuō)一下:主隊(duì)列和主線程的關(guān)系。
(1)主隊(duì)列是專門(mén)負(fù)責(zé)調(diào)度主線程的任務(wù)的。iOS編程中,需要在主線程(主線程,這個(gè)線程是其他線程的父線程)中進(jìn)行操作時(shí),我們經(jīng)常會(huì)用到以下代碼:
dispatch_async(dispatch_get_main_queue(), ^{
// dispatch_get_main_queue() 實(shí)際獲取的是 主隊(duì)列
});
而且在主隊(duì)列下的任務(wù)不管是異步任務(wù)還是同步任務(wù)都不會(huì)開(kāi)辟線程,任務(wù)只會(huì)在主線程順序執(zhí)行。比如block內(nèi)的任務(wù)是異步執(zhí)行,主線程在將當(dāng)前方法執(zhí)行完畢之后,才會(huì)去繼續(xù)執(zhí)行主隊(duì)列里的任務(wù)。
(2)主隊(duì)列的任務(wù)一定在主線程中執(zhí)行,主線程是可以執(zhí)行主隊(duì)列之外(dispatch_get_global_queue)其他隊(duì)列的任務(wù)的。
另外關(guān)于主線程中更新UI操作也不是絕對(duì)安全的,詳細(xì)請(qǐng)看這篇文章:主線程中也不絕對(duì)安全的 UI 操作
-
全局隊(duì)列:本質(zhì)是一個(gè)并發(fā)隊(duì)列,由系統(tǒng)提供,是所有應(yīng)用程序共享的。方便編程,可以不用創(chuàng)建就直接使用。
#獲取全局隊(duì)列的方法: dispatch_get_global_queue(long indentifier.unsigned long flags) 第一個(gè)參數(shù):線程優(yōu)先級(jí),默認(rèn)寫(xiě)0就行,不要使用系統(tǒng)提供的枚舉類(lèi)型,因?yàn)閕os7和ios8的枚舉數(shù)值不一樣,使用數(shù)字可以通用。 第二個(gè)參數(shù):標(biāo)記參數(shù),目前沒(méi)有用,一般傳入0. #使用全局隊(duì)列多線程執(zhí)行任務(wù) dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); #創(chuàng)建多個(gè)線程用于填充圖片 for (int i=0; i<count; ++i) { #異步執(zhí)行隊(duì)列任務(wù) dispatch_async(globalQueue, ^{ [self loadImage:[NSNumber numberWithInt:i]]; }); }
隊(duì)列組
隊(duì)列組可以將很多隊(duì)列添加到一個(gè)組里,這樣做的好處是,當(dāng)這個(gè)組里所有的任務(wù)都執(zhí)行完了,隊(duì)列組會(huì)通過(guò)一個(gè)方法通知我們。這是一個(gè)很實(shí)用的功能。
# 創(chuàng)建隊(duì)列組
dispatch_group_t group = dispatch_group_create();
# 獲取到全局隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
# 多次使用隊(duì)列組的方法執(zhí)行任務(wù), 目前API中只有異步方法
//1.執(zhí)行3次循環(huán)
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
//2.主隊(duì)列執(zhí)行8次循環(huán)
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 8; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});
//3.執(zhí)行5次循環(huán)
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
#.都完成后會(huì)自動(dòng)通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成 - %@", [NSThread currentThread]);
});
隊(duì)列組其他實(shí)用方法
這也是使用GCD信號(hào)量實(shí)現(xiàn)多線程同步加鎖的實(shí)現(xiàn)方式。
dispatch_group_enter
用于添加對(duì)應(yīng)任務(wù)組中的未執(zhí)行完畢的任務(wù)數(shù),執(zhí)行一次,未執(zhí)行完畢的任務(wù)數(shù)加1,當(dāng)未執(zhí)行完畢任務(wù)數(shù)為0的時(shí)候,才會(huì)使dispatch_group_wait解除阻塞和dispatch_group_notify的block執(zhí)行
void dispatch_group_enter(dispatch_group_t group);
dispatch_group_leave
用于減少任務(wù)組中的未執(zhí)行完畢的任務(wù)數(shù),執(zhí)行一次,未執(zhí)行完畢的任務(wù)數(shù)減1,dispatch_group_enter和dispatch_group_leave要匹配,不然系統(tǒng)會(huì)認(rèn)為group任務(wù)沒(méi)有執(zhí)行完畢
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_wait
等待組任務(wù)完成,會(huì)阻塞當(dāng)前線程,當(dāng)任務(wù)組執(zhí)行完畢時(shí),才會(huì)解除阻塞當(dāng)前線程
long dispatch_group_wait(dispatch_group_t group,
dispatch_time_t timeout);
***********************************
group ——需要等待的任務(wù)組
timeout ——等待的超時(shí)時(shí)間(即等多久),單位為dispatch_time_t。
如果設(shè)置為DISPATCH_TIME_FOREVER,則會(huì)一直等待(阻塞當(dāng)前線程),直到任務(wù)組執(zhí)行完畢。
信號(hào)量
當(dāng)我們?cè)谔幚硪幌盗芯€程的時(shí)候,當(dāng)數(shù)量達(dá)到一定量,在以前我們可能會(huì)選擇使用NSOperationQueue來(lái)處理并發(fā)控制,但如何在GCD中快速的控制并發(fā)呢?答案就是
dispatch_semaphore.
信號(hào)量是一個(gè)整形值并且具有一個(gè)初始計(jì)數(shù)值,并且支持兩個(gè)操作:信號(hào)通知和等待。當(dāng)一個(gè)信號(hào)量被信號(hào)通知,其計(jì)數(shù)會(huì)被增加。當(dāng)一個(gè)線程在一個(gè)信號(hào)量上等待時(shí),線程會(huì)被阻塞(如果有必要的話),直至計(jì)數(shù)器大于零,然后線程會(huì)減少這個(gè)計(jì)數(shù)。
在GCD中有三個(gè)函數(shù)是semaphore的操作,分別是:
dispatch_semaphore_create 創(chuàng)建一個(gè)semaphore
dispatch_semaphore_signal 發(fā)送一個(gè)信號(hào)
dispatch_semaphore_wait 等待信號(hào)
第一個(gè)函數(shù)有一個(gè)整形的參數(shù),我們可以理解為信號(hào)的總量;
dispatch_semaphore_signal是發(fā)送一個(gè)信號(hào),自然會(huì)讓信號(hào)總量+1,
dispatch_semaphore_wait等待信號(hào),當(dāng)信號(hào)總量少于0的時(shí)候就會(huì)一直等待,否則就可以正常的執(zhí)行,并讓信號(hào)總量-1,特別說(shuō)明下:信號(hào)總量為0時(shí)dispatch_semaphore_wait會(huì)阻塞當(dāng)前的線程(主線程、其他線程),被阻塞的線程中無(wú)法執(zhí)行任何代碼。
根據(jù)這樣的原理,我們便可以快速的創(chuàng)建一個(gè)并發(fā)控制來(lái)同步任務(wù)和有限資源訪問(wèn)控制。
使用GCD的信號(hào)量實(shí)現(xiàn)并發(fā)的控制
創(chuàng)建了一個(gè)初使值為10的semaphore,每一次for循環(huán)都會(huì)創(chuàng)建一個(gè)新的線程,線程結(jié)束的時(shí)候會(huì)發(fā)送一個(gè)信號(hào),線程創(chuàng)建之前會(huì)信號(hào)等待,所以當(dāng)同時(shí)創(chuàng)建了10個(gè)線程之后,for循環(huán)就會(huì)阻塞,等待有線程結(jié)束之后會(huì)增加一個(gè)信號(hào)才繼續(xù)執(zhí)行,如此就形成了對(duì)并發(fā)的控制,如上就是一個(gè)并發(fā)數(shù)為10的一個(gè)線程隊(duì)列。
GCD執(zhí)行任務(wù)的其他一些常用方法
#重復(fù)執(zhí)行某個(gè)任務(wù),但是注意這個(gè)方法沒(méi)有辦法異步執(zhí)行(為了不阻塞線程可以使用dispatch_async()包裝一下再執(zhí)行)。
dispatch_apply():
#單次執(zhí)行一個(gè)任務(wù),此方法中的任務(wù)只會(huì)執(zhí)行一次,重復(fù)調(diào)用也沒(méi)辦法重復(fù)執(zhí)行(單例模式中常用此方法)。
dispatch_once():
#延遲一定的時(shí)間后執(zhí)行。
dispatch_time():
#使用此方法創(chuàng)建的任務(wù)首先會(huì)查看隊(duì)列中有沒(méi)有別的任務(wù)要執(zhí)行,如果有,則會(huì)等待已有任務(wù)執(zhí)行完畢再執(zhí)行;同時(shí)在此方法后添加的任務(wù)必須等待此方法中任務(wù)執(zhí)行后才能執(zhí)行。(利用這個(gè)方法可以控制執(zhí)行順序,例如前面先加載最后一張圖片的需求就可以先使用這個(gè)方法將最后一張圖片加載的操作添加到隊(duì)列,然后調(diào)用dispatch_async()添加其他圖片加載任務(wù))
dispatch_barrier_async():
#實(shí)現(xiàn)對(duì)任務(wù)分組管理,如果一組任務(wù)全部完成可以通過(guò)
dispatch_group_async():
#方法獲得完成通知(需要定義dispatch_group_t作為分組標(biāo)識(shí))。
dispatch_group_notify()
一個(gè)栗子:多個(gè)并發(fā)網(wǎng)絡(luò)請(qǐng)求完成后執(zhí)行下一步
-
1.使用GCD的dispatch_group_t
-(void)Btn2{ NSString *str = @"http://www.lxweimin.com/p/6930f335adba"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; dispatch_group_t downloadGroup = dispatch_group_create(); for (int i=0; i<10; i++) { dispatch_group_enter(downloadGroup); NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%d---%d",i,i); dispatch_group_leave(downloadGroup); }]; [task resume]; } dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ NSLog(@"end"); }); }
創(chuàng)建一個(gè)dispatch_group_t, 每次網(wǎng)絡(luò)請(qǐng)求前先dispatch_group_enter,請(qǐng)求回調(diào)后再dispatch_group_leave,對(duì)于enter和leave必須配合使用,有幾次enter就要有幾次leave,否則group會(huì)一直存在。當(dāng)所有enter的block都leave后,會(huì)執(zhí)行dispatch_group_notify的block。
-
2.使用GCD的信號(hào)量dispatch_semaphore_t
-(void)Btn3{ NSString *str = @"http://www.lxweimin.com/p/6930f335adba"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; dispatch_semaphore_t sem = dispatch_semaphore_create(0); for (int i=0; i<10; i++) { NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@",[NSThread currentThread]) NSLog(@"%d---%d",i,i); count++; if (count==10) { dispatch_semaphore_signal(sem); count = 0; } }]; [task resume]; } dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"end"); }); }
開(kāi)始信號(hào)量為0,等待,等10個(gè)網(wǎng)絡(luò)請(qǐng)求都完成了,dispatch_semaphore_signal(semaphore)為計(jì)數(shù)+1,然后計(jì)數(shù)-1返回,程序繼續(xù)執(zhí)行。
這里特別說(shuō)一下,例子中的NSURLSessionDataTask 的block 回調(diào)中不是主線程,而是多線程環(huán)境。此時(shí)的主線程已經(jīng)被阻塞了,是不會(huì)執(zhí)行任何代碼的,只有在子線程中把信號(hào)量加1,才能結(jié)束主線程的阻塞。
10個(gè)網(wǎng)絡(luò)請(qǐng)求順序回調(diào)。
- (void)toCrashing { NSString *str = @"http://www.lxweimin.com/p/6930f335adba"; NSURL *url = [NSURL URLWithString:str]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSession *session = [NSURLSession sharedSession]; NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0]; dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); for (int i=0; i<10; i++) { NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { dispatch_async(queue, ^{ [lock lockWhenCondition:i]; NSLog(@"%d---%d",i,i); [lock unlockWithCondition:i+1]; }); }]; [task resume]; } }
使用NSConditionLock 可以解決。
小結(jié)
GCD的知識(shí)很多,本文就不一一羅列了,后續(xù)如有新的收獲,會(huì)持續(xù)更新的。
本文參考文章:
iOS編程中throttle那些事
關(guān)于iOS多線程,你看我就夠了
GCD入門(mén)(二): 多核心的性能