前言
GCD是蘋果為多核的并行運算提出的解決方案米绕,所以會自動合理地利用更多的CPU內(nèi)核(比如雙核、四核)菇存,最重要的是它會自動管理線程的生命周期(創(chuàng)建線程却盘、調(diào)度任務(wù)狰域、銷毀線程),完全不需要我們管理黄橘,我們只需要告訴干什么就行兆览。同時GCD抽象層次最高,當(dāng)然是用起來也最簡單旬陡,只是它基于C語言開發(fā)拓颓,并不像NSOperation是面向?qū)ο蟮拈_發(fā)语婴,而是完全面向過程的描孟。
GCD是一種輕量的基于block的線程模型驶睦,底層實現(xiàn)主要有Dispatch Queue和Dispatch Source
Dispatch Queue :管理block(操作)
Dispatch Source :處理事件
Dispatch Queue&Dispatch Source更多相關(guān)知識
GCD推出來以后,開發(fā)者可以不直接操縱線程匿醒,而是將所要執(zhí)行的任務(wù)封裝成一個unit丟給線程池去處理场航,線程池會有效管理線程的并發(fā),控制線程的生死廉羔。因此溉痢,現(xiàn)在如果考慮到并發(fā)場景,基本上是圍繞著GCD和NSOperationQueue來展開討論憋他。
任務(wù)和隊列
這里就不得不提到兩個概念:任務(wù)和隊列
- 任務(wù):即操作孩饼,就是一段代碼,在 GCD 中就是一個 Block竹挡,所以添加任務(wù)十分方便镀娶。調(diào)度隊列執(zhí)行任務(wù)有兩種方式: 同步執(zhí)行 和 異步執(zhí)行.
同步派發(fā)(sync) 和 異步派發(fā)(async) 的主要區(qū)別在于會不會阻塞當(dāng)前線程,直到 Block 中的任務(wù)執(zhí)行完畢揪罕!
【異步并不一定會開啟多線程,當(dāng)在主線程中派發(fā)任務(wù)到主隊列后梯码,會等待主線程空閑時才會調(diào)度該任務(wù)并沒有開啟新的線程;添加到其他線程時好啰,會開啟新的線程調(diào)度任務(wù)轩娶∶拔】
如果是 同步(sync) 操作掂碱,它會阻塞當(dāng)前線程并等待 Block 中的任務(wù)執(zhí)行完畢焦蘑,然后當(dāng)前線程才會繼續(xù)往下運行厚宰。
如果是 異步(async)操作辕翰,當(dāng)前線程會直接往下執(zhí)行罚屋,它不會阻塞當(dāng)前線程徙硅。
- 隊列:調(diào)度隊列是一個類似對象的結(jié)構(gòu)體牙肝,它管理您提交給它的任務(wù)男应。所有的調(diào)度隊列都是先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)闹司。隊列和線程的區(qū)別,他們之間并沒有“擁有關(guān)系(ownership)”沐飘。隊列用于存放任務(wù)游桩。一共有兩種隊列, 串行隊列 和 并行隊列耐朴。
放到串行隊列的任務(wù)借卧,GCD 會 FIFO(先進(jìn)先出) 地取出來一個,執(zhí)行一個筛峭,然后取下一個铐刘,這樣一個一個的執(zhí)行。
放到并行隊列的任務(wù)影晓,GCD 也會 FIFO的取出來镰吵,但不同的是檩禾,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程疤祭。這樣由于取的動作很快盼产,忽略不計,看起來勺馆,所有的任務(wù)都是一起執(zhí)行的戏售。不過需要注意,GCD 會根據(jù)系統(tǒng)資源控制并行的數(shù)量草穆,所以如果任務(wù)很多灌灾,它并不會讓所有任務(wù)同時執(zhí)行。
GCD中不同隊列中不同任務(wù)的執(zhí)行情況如下表:
同步執(zhí)行的任務(wù) | 異步執(zhí)行的任務(wù) | |
---|---|---|
串行隊列中 | 當(dāng)前線程悲柱,一個一個執(zhí)行 | 其他線程紧卒,一個一個執(zhí)行 |
并行隊列中 | 當(dāng)前線程,一個一個執(zhí)行 | 同時開很多線程诗祸,一起執(zhí)行 |
隊列的創(chuàng)建和執(zhí)行
#開辟隊列的方法:
dispatch_queue_t myQueue = dispatch_queue_create("MyQueue", NULL);
參數(shù)1:標(biāo)簽跑芳,用于區(qū)分隊列
參數(shù)2:隊列的類型,表示這個隊列是串行隊列還是并發(fā)隊列NUll表示串行隊列直颅,
DISPATCH_QUEUE_SERIAL 或 NULL 表示創(chuàng)建串行隊列博个。
DISPATCH_QUEUE_CONCURRENT 表示創(chuàng)建并行隊列。
#執(zhí)行隊列的方法
異步執(zhí)行
dispatch_async(<#dispatch_queue_t queue#>, <#^(void)block#>)
同步執(zhí)行
dispatch_sync(<#dispatch_queue_t queue#>, <#^(void)block#>)
幾個特別的隊列
- 主隊列(Main Queue):專門負(fù)責(zé)調(diào)度主線程(Main Thread)的任務(wù)功偿,是一個串行隊列盆佣,沒有辦法開辟新的線程。任何需要刷新 UI 的工作都要在主隊列執(zhí)行械荷,所以一般耗時的任務(wù)都要放到別的線程執(zhí)行共耍。
這里需要特別說一下:主隊列和主線程的關(guān)系。
(1)主隊列是專門負(fù)責(zé)調(diào)度主線程的任務(wù)的吨瞎。iOS編程中痹兜,需要在主線程(主線程,這個線程是其他線程的父線程)中進(jìn)行操作時颤诀,我們經(jīng)常會用到以下代碼:
dispatch_async(dispatch_get_main_queue(), ^{
// dispatch_get_main_queue() 實際獲取的是 主隊列
});
而且在主隊列下的任務(wù)不管是異步任務(wù)還是同步任務(wù)都不會開辟線程字旭,任務(wù)只會在主線程順序執(zhí)行。比如block內(nèi)的任務(wù)是異步執(zhí)行崖叫,主線程在將當(dāng)前方法執(zhí)行完畢之后遗淳,才會去繼續(xù)執(zhí)行主隊列里的任務(wù)。
(2)主隊列的任務(wù)一定在主線程中執(zhí)行心傀,主線程是可以執(zhí)行主隊列之外(dispatch_get_global_queue)其他隊列的任務(wù)的屈暗。
另外關(guān)于主線程中更新UI操作也不是絕對安全的,詳細(xì)請看這篇文章:主線程中也不絕對安全的 UI 操作
-
全局隊列:本質(zhì)是一個并發(fā)隊列,由系統(tǒng)提供养叛,是所有應(yīng)用程序共享的种呐。方便編程,可以不用創(chuàng)建就直接使用一铅。
#獲取全局隊列的方法: dispatch_get_global_queue(long indentifier.unsigned long flags) 第一個參數(shù):線程優(yōu)先級,默認(rèn)寫0就行陕贮,不要使用系統(tǒng)提供的枚舉類型堕油,因為ios7和ios8的枚舉數(shù)值不一樣潘飘,使用數(shù)字可以通用。 第二個參數(shù):標(biāo)記參數(shù)掉缺,目前沒有用卜录,一般傳入0. #使用全局隊列多線程執(zhí)行任務(wù) dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); #創(chuàng)建多個線程用于填充圖片 for (int i=0; i<count; ++i) { #異步執(zhí)行隊列任務(wù) dispatch_async(globalQueue, ^{ [self loadImage:[NSNumber numberWithInt:i]]; }); }
隊列組
隊列組可以將很多隊列添加到一個組里,這樣做的好處是眶明,當(dāng)這個組里所有的任務(wù)都執(zhí)行完了艰毒,隊列組會通過一個方法通知我們。這是一個很實用的功能搜囱。
# 創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
# 獲取到全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
# 多次使用隊列組的方法執(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.主隊列執(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]);
}
});
#.都完成后會自動通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成 - %@", [NSThread currentThread]);
});
隊列組其他實用方法
這也是使用GCD信號量實現(xiàn)多線程同步加鎖的實現(xiàn)方式丑瞧。
dispatch_group_enter
用于添加對應(yīng)任務(wù)組中的未執(zhí)行完畢的任務(wù)數(shù),執(zhí)行一次蜀肘,未執(zhí)行完畢的任務(wù)數(shù)加1绊汹,當(dāng)未執(zhí)行完畢任務(wù)數(shù)為0的時候,才會使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)會認(rèn)為group任務(wù)沒有執(zhí)行完畢
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_wait
等待組任務(wù)完成获雕,會阻塞當(dāng)前線程,當(dāng)任務(wù)組執(zhí)行完畢時收捣,才會解除阻塞當(dāng)前線程
long dispatch_group_wait(dispatch_group_t group,
dispatch_time_t timeout);
***********************************
group ——需要等待的任務(wù)組
timeout ——等待的超時時間(即等多久)届案,單位為dispatch_time_t。
如果設(shè)置為DISPATCH_TIME_FOREVER,則會一直等待(阻塞當(dāng)前線程)罢艾,直到任務(wù)組執(zhí)行完畢萝玷。
信號量
當(dāng)我們在處理一系列線程的時候,當(dāng)數(shù)量達(dá)到一定量昆婿,在以前我們可能會選擇使用NSOperationQueue來處理并發(fā)控制球碉,但如何在GCD中快速的控制并發(fā)呢?答案就是
dispatch_semaphore.
信號量是一個整形值并且具有一個初始計數(shù)值仓蛆,并且支持兩個操作:信號通知和等待睁冬。當(dāng)一個信號量被信號通知,其計數(shù)會被增加。當(dāng)一個線程在一個信號量上等待時豆拨,線程會被阻塞(如果有必要的話)直奋,直至計數(shù)器大于零,然后線程會減少這個計數(shù)施禾。
在GCD中有三個函數(shù)是semaphore的操作脚线,分別是:
dispatch_semaphore_create 創(chuàng)建一個semaphore
dispatch_semaphore_signal 發(fā)送一個信號
dispatch_semaphore_wait 等待信號
第一個函數(shù)有一個整形的參數(shù),我們可以理解為信號的總量弥搞;
dispatch_semaphore_signal是發(fā)送一個信號邮绿,自然會讓信號總量+1,
dispatch_semaphore_wait等待信號攀例,當(dāng)信號總量少于0的時候就會一直等待船逮,否則就可以正常的執(zhí)行,并讓信號總量-1粤铭,特別說明下:信號總量為0時dispatch_semaphore_wait會阻塞當(dāng)前的線程(主線程挖胃、其他線程),被阻塞的線程中無法執(zhí)行任何代碼梆惯。
根據(jù)這樣的原理酱鸭,我們便可以快速的創(chuàng)建一個并發(fā)控制來同步任務(wù)和有限資源訪問控制。
使用GCD的信號量實現(xiàn)并發(fā)的控制
創(chuàng)建了一個初使值為10的semaphore垛吗,每一次for循環(huán)都會創(chuàng)建一個新的線程凹髓,線程結(jié)束的時候會發(fā)送一個信號,線程創(chuàng)建之前會信號等待职烧,所以當(dāng)同時創(chuàng)建了10個線程之后扁誓,for循環(huán)就會阻塞,等待有線程結(jié)束之后會增加一個信號才繼續(xù)執(zhí)行蚀之,如此就形成了對并發(fā)的控制蝗敢,如上就是一個并發(fā)數(shù)為10的一個線程隊列。
GCD執(zhí)行任務(wù)的其他一些常用方法
#重復(fù)執(zhí)行某個任務(wù)足删,但是注意這個方法沒有辦法異步執(zhí)行(為了不阻塞線程可以使用dispatch_async()包裝一下再執(zhí)行)寿谴。
dispatch_apply():
#單次執(zhí)行一個任務(wù),此方法中的任務(wù)只會執(zhí)行一次失受,重復(fù)調(diào)用也沒辦法重復(fù)執(zhí)行(單例模式中常用此方法)讶泰。
dispatch_once():
#延遲一定的時間后執(zhí)行。
dispatch_time():
#使用此方法創(chuàng)建的任務(wù)首先會查看隊列中有沒有別的任務(wù)要執(zhí)行拂到,如果有痪署,則會等待已有任務(wù)執(zhí)行完畢再執(zhí)行;同時在此方法后添加的任務(wù)必須等待此方法中任務(wù)執(zhí)行后才能執(zhí)行兄旬。(利用這個方法可以控制執(zhí)行順序狼犯,例如前面先加載最后一張圖片的需求就可以先使用這個方法將最后一張圖片加載的操作添加到隊列,然后調(diào)用dispatch_async()添加其他圖片加載任務(wù))
dispatch_barrier_async():
#實現(xiàn)對任務(wù)分組管理,如果一組任務(wù)全部完成可以通過
dispatch_group_async():
#方法獲得完成通知(需要定義dispatch_group_t作為分組標(biāo)識)悯森。
dispatch_group_notify()
一個栗子:多個并發(fā)網(wǎng)絡(luò)請求完成后執(zhí)行下一步
-
1.使用GCD的dispatch_group_t
-(void)Btn2{ NSString *str = @"http://www.reibang.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)建一個dispatch_group_t宋舷, 每次網(wǎng)絡(luò)請求前先dispatch_group_enter,請求回調(diào)后再dispatch_group_leave,對于enter和leave必須配合使用,有幾次enter就要有幾次leave瓢姻,否則group會一直存在祝蝠。當(dāng)所有enter的block都leave后,會執(zhí)行dispatch_group_notify的block幻碱。
-
2.使用GCD的信號量dispatch_semaphore_t
-(void)Btn3{ NSString *str = @"http://www.reibang.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"); }); }
開始信號量為0绎狭,等待,等10個網(wǎng)絡(luò)請求都完成了收班,dispatch_semaphore_signal(semaphore)為計數(shù)+1坟岔,然后計數(shù)-1返回谒兄,程序繼續(xù)執(zhí)行摔桦。
這里特別說一下,例子中的NSURLSessionDataTask 的block 回調(diào)中不是主線程承疲,而是多線程環(huán)境邻耕。此時的主線程已經(jīng)被阻塞了,是不會執(zhí)行任何代碼的燕鸽,只有在子線程中把信號量加1兄世,才能結(jié)束主線程的阻塞。
10個網(wǎng)絡(luò)請求順序回調(diào)啊研。
- (void)toCrashing { NSString *str = @"http://www.reibang.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的知識很多,本文就不一一羅列了党远,后續(xù)如有新的收獲削解,會持續(xù)更新的。
本文參考文章:
iOS編程中throttle那些事
關(guān)于iOS多線程沟娱,你看我就夠了
GCD入門(二): 多核心的性能