多線程編程是一項(xiàng)非常重要的技術(shù),目前在iOS開(kāi)發(fā)中比較流行的多線程方案是GCD和NSOperationQueue,本文將詳細(xì)介紹如何使用GCD進(jìn)行多線程編程。根據(jù)蘋(píng)果的文檔,GCD(Grand Central Dispatch)是一項(xiàng)提供了管理并發(fā)和異步執(zhí)行任務(wù)的技術(shù),開(kāi)發(fā)者只需要將想要執(zhí)行的任務(wù)追加到適當(dāng)?shù)膁ispatch queue中,GCD會(huì)為此生成必要的線程來(lái)執(zhí)行任務(wù)。GCD使用非常簡(jiǎn)潔的語(yǔ)法實(shí)現(xiàn)了復(fù)雜的多線程編程方案,接下來(lái)我們深入認(rèn)識(shí)它。
dispatch queue
GCD使用隊(duì)列來(lái)實(shí)現(xiàn)多個(gè)任務(wù)的執(zhí)行,隊(duì)列分為兩種,serial dispatch queue和concurrent dispatch queue,前者指隊(duì)列中的任務(wù)會(huì)使用單一的線程逐個(gè)執(zhí)行,而后者指隊(duì)列中的任務(wù)會(huì)使用多個(gè)線程并行執(zhí)行。我們可以自行創(chuàng)建這兩種隊(duì)列,方法如下:
dispatch_queue_t serialQueue = dispatch_queue_create(@"cn.test.serial.queue", NULL);
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create函數(shù)用于創(chuàng)建一個(gè)隊(duì)列,方法的第一個(gè)參數(shù)為隊(duì)列的標(biāo)簽(可為空,但并不推薦這樣做),第二個(gè)參數(shù)指定隊(duì)列的類(lèi)型,當(dāng)指定為NULL時(shí)即指serial dispatch queue,指定為DISPATCH_QUEUE_CONCURRENT時(shí)為concurrent dispatch queue,需要注意的是iOS6之后,GCD已經(jīng)納入了ARC的管理范圍,所以不再需要手動(dòng)調(diào)用相關(guān)retain/release方法。
事實(shí)上,蘋(píng)果為我們提供了默認(rèn)的dispatch queue,供我們使用,因此自行創(chuàng)建dispatch queue并不總是必要的。默認(rèn)的隊(duì)列有兩種main dispatch queue和global dispatch queue,前者隊(duì)列中的任務(wù)會(huì)在主線程中去執(zhí)行,主線程只有一個(gè),所以它是serial dispatch queue,而后者則屬于concurrent dispatch queue,隊(duì)列中的任務(wù)會(huì)并發(fā)執(zhí)行,此隊(duì)列有四種優(yōu)先級(jí)可供選擇,分別為DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND ,使用默認(rèn)隊(duì)列的方式是這樣的:
//獲取main dispatch queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//獲取global dispatch queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_get_global_queue函數(shù)的第一個(gè)參數(shù)指定隊(duì)列優(yōu)先級(jí),第二個(gè)參數(shù)默認(rèn)寫(xiě)0即可
dispatch_async
我們使用dispatch_async向某個(gè)隊(duì)列追加一項(xiàng)任務(wù),async表明該方式是異步的,不會(huì)阻塞當(dāng)前線程,使用方式如下
dispatch_async(serialQueue, ^ {
NSLog(@"block");
});
該函數(shù)第一個(gè)參數(shù)指定向哪個(gè)隊(duì)列追加任務(wù),第二個(gè)參數(shù)是一個(gè)block,表示要執(zhí)行的任務(wù)。
dispatch_set_target_queue
該函數(shù)有兩個(gè)功能,第一,可以指定某個(gè)隊(duì)列的優(yōu)先級(jí)和目標(biāo)隊(duì)列優(yōu)先級(jí)一致,例如
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(concurrentQueue, globalQueue);
我們創(chuàng)建的隊(duì)列都是默認(rèn)優(yōu)先級(jí),上述代碼表示指定concurrentQueue優(yōu)先級(jí)和globalQueue優(yōu)先級(jí)一致。
第二,可用于改變隊(duì)列的執(zhí)行層次。舉個(gè)例子,現(xiàn)有A,B, C三個(gè)serial dispatch queue,如果指定這三個(gè)隊(duì)列的target queue為D serial dispatch queue,那么原本會(huì)并行執(zhí)行的三個(gè)隊(duì)列在D上就會(huì)串行執(zhí)行,示例如下:
dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue",NULL);
dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"1 in");
[NSThread sleepForTimeInterval:3.f];
NSLog(@"1 out");
});
dispatch_async(queue2, ^{
NSLog(@"2 in");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"2 out");
});
dispatch_async(queue3, ^{
NSLog(@"3 in");
[NSThread sleepForTimeInterval:1.f];
NSLog(@"3 out");
});
//代碼執(zhí)行結(jié)果
1 in
1 out
2 in
2 out
3 in
3 out
dispatch_after
該函數(shù)用于延時(shí)調(diào)用,例如當(dāng)我們想要延時(shí)3秒執(zhí)行某項(xiàng)任務(wù)時(shí):
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"test");
});
其中第一個(gè)參數(shù)是時(shí)間,類(lèi)型為dispatch_time_t,上例中直接使用dispatch_time函數(shù)生成,表示從現(xiàn)在開(kāi)始3秒之后的時(shí)間,第二個(gè)參數(shù)是要在哪個(gè)隊(duì)列中執(zhí)行,上例為main dispatch queue,第三個(gè)參數(shù)為要執(zhí)行的block,該函數(shù)實(shí)際上是在3秒之后向隊(duì)列追加了一個(gè)block任務(wù)。
dispatch_group
當(dāng)我們有多個(gè)線程在同時(shí)執(zhí)行任務(wù),我們希望在所有線程中的任務(wù)執(zhí)行完畢后做某項(xiàng)處理,那么就可以將前面的多個(gè)線程加入到一個(gè)group中,group會(huì)監(jiān)控線程中的任務(wù)是否執(zhí)行完畢,執(zhí)行完畢可以發(fā)送通知,之后我們就可以做某項(xiàng)處理。示例如下:
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("cn.test.serial.queue2", NULL);
dispatch_queue_t serialQueue3 = dispatch_queue_create("cn.test.serial.queue3", NULL);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, serialQueue1, ^ {
[NSThread sleepForTimeInterval:2];
NSLog(@"test");
});
dispatch_group_async(group, serialQueue2, ^ {
[NSThread sleepForTimeInterval:3];
NSLog(@"test2");
});
dispatch_group_async(group, serialQueue3, ^ {
[NSThread sleepForTimeInterval:1];
NSLog(@"test3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^ {
NSLog(@"finished");
});
//代碼執(zhí)行結(jié)果
test3
test
test2
finished
dispatch_group_notify函數(shù)會(huì)在group中所有線程中的任務(wù)執(zhí)行完畢后被調(diào)用,因此上例不論前面線程執(zhí)行誰(shuí)先誰(shuí)后,最后執(zhí)行的一定是主線程中的block。例如有時(shí)候我們會(huì)通過(guò)多線程的方式去下載一些資源,數(shù)據(jù)等,然后所有下載完畢后在主線程中進(jìn)行UI更新,就可以這樣用。
除了使用dispatch_group_notify來(lái)等待group中所有任務(wù)執(zhí)行完畢外,還可以通過(guò)dispatch_group_wait函數(shù)來(lái)實(shí)現(xiàn)同樣效果
long ret = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if (ret == 0) {
NSLog(@"finished");
}else
{
NSLog(@"unfinished");
}
需要說(shuō)明的是該函數(shù)第二個(gè)參數(shù)是指等待時(shí)間,wait函數(shù)會(huì)使當(dāng)前線程停止,等待直到:
- 在等待時(shí)間內(nèi),group中所有任務(wù)執(zhí)行完畢,返回0
- 在等待時(shí)間內(nèi),group中任務(wù)沒(méi)有執(zhí)行完畢,超時(shí)返回非0
如上例所示,當(dāng)設(shè)置時(shí)間為DISPATCH_TIME_FOREVER意味著永久等待,直到任務(wù)執(zhí)行完畢。
dispatch_barrier_async
當(dāng)我們需要多個(gè)線程對(duì)某個(gè)屬性進(jìn)行頻繁的讀寫(xiě)操作時(shí),如果不對(duì)線程加以控制,很容易造成讀寫(xiě)混亂,因此我們可能希望:當(dāng)進(jìn)行讀操作時(shí),可以多個(gè)線程同時(shí)讀取以保證高效率,而進(jìn)行寫(xiě)操作時(shí),不可以有任何線程進(jìn)行讀操作,寫(xiě)入完畢,線程又可以并發(fā)讀取屬性值,這種情況barrier技術(shù)正好可以方便的解決問(wèn)題,示例如下
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^ { /*讀取*/ });
dispatch_async(concurrentQueue, ^ { /*讀取*/ });
dispatch_async(concurrentQueue, ^ { /*讀取*/ });
dispatch_barrier_async(concurrentQueue, ^ { /*寫(xiě)入*/});
dispatch_async(concurrentQueue, ^ { /*讀取*/ });
dispatch_async(concurrentQueue, ^ { /*讀取*/ });
dispatch_sync
有dispatch_async,當(dāng)然也就有dispatch_sync,該函數(shù)表示同步執(zhí)行,即當(dāng)前線程會(huì)等待sync中的任務(wù)執(zhí)行完畢才繼續(xù)往下執(zhí)行,例如
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_sync(serialQueue1, ^ {
NSLog(@"test");
});
在主線程中執(zhí)行上述代碼,當(dāng)執(zhí)行到dispatch_sync時(shí),線程會(huì)進(jìn)行等待,直到追加到serialQueue1中的block執(zhí)行完畢,主線程才繼續(xù)往下執(zhí)行。這種同步調(diào)用如果不加注意,就容易造成線程死鎖問(wèn)題,例如在主線程中執(zhí)行下述代碼就會(huì)造成死鎖問(wèn)題,主線程停止,等待lock執(zhí)行完畢,而追加到主線程的block因?yàn)橹骶€程停止而永遠(yuǎn)不會(huì)被執(zhí)行,于是產(chǎn)生死鎖。
dispatch_sync(dispatch_get_main_queue(), ^ {
NSLog(@"test");
});
dispatch_apply
該函數(shù)與dispatch_sync有些關(guān)聯(lián),是指重復(fù)向某個(gè)隊(duì)列追加block并等待全部block執(zhí)行完畢后返回。例如當(dāng)我們想遍歷某個(gè)數(shù)組,對(duì)所有元素做些操作,可能會(huì)使用循環(huán)的方式,當(dāng)數(shù)組非常大的時(shí)候,我們可能希望使用多線程去遍歷數(shù)組所有元素來(lái)提高效率,那么就可以使用dispatch_apply,示例如下
NSArray *array = /*數(shù)組賦值*/;
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply([array count], concurrentQueue, ^(size_t index) {
NSLog(@"array item: %@", [array objectAtIndex:index]);
});
dispatch_apply的第一個(gè)參數(shù)指重復(fù)追加的次數(shù),第二個(gè)為執(zhí)行的隊(duì)列,第三個(gè)為要執(zhí)行的block,和前面不同的是,block里有一個(gè)參數(shù),這個(gè)參數(shù)用來(lái)標(biāo)識(shí)追加到隊(duì)列中的每個(gè)block。上述代碼中所有block在執(zhí)行完畢后才能執(zhí)行后續(xù)代碼。
dispatch_suspend/dispatch_resume
這一對(duì)函數(shù)非常簡(jiǎn)單,dispatch_suspend用于掛起隊(duì)列,之后隊(duì)列中所有任務(wù)都會(huì)暫停執(zhí)行,dispatch_resume使隊(duì)列從暫停狀態(tài)恢復(fù)為繼續(xù)執(zhí)行狀態(tài)。
dispatch semaphore
dispatch_semaphore可以實(shí)現(xiàn)更加精細(xì)化地對(duì)線程進(jìn)行管理。semaphore是擁有計(jì)數(shù)的信號(hào)量,我們可以通過(guò)semaphore對(duì)多線程訪問(wèn)共享資源時(shí)進(jìn)行精細(xì)化的排他控制,也可以通過(guò)semaphore實(shí)現(xiàn)并發(fā)線程數(shù)量的控制。
為了便于理解,我們舉一個(gè)停車(chē)的例子,假設(shè)停車(chē)場(chǎng)只剩下兩個(gè)車(chē)位,這時(shí)候同時(shí)來(lái)了三輛車(chē),那么勢(shì)必只能開(kāi)入兩輛車(chē),另外一輛需要先一旁等待,直到有車(chē)離開(kāi)停車(chē)位,等待的車(chē)才能開(kāi)入,我們用信號(hào)量來(lái)實(shí)現(xiàn)這段邏輯:
dispatch_semaphore_t sema = dispatch_semaphore_create(2);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 1");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 1");
dispatch_semaphore_signal(sema);
});
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 2");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 2");
dispatch_semaphore_signal(sema);
});
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 3");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 3");
dispatch_semaphore_signal(sema);
});
//代碼執(zhí)行結(jié)果
2017-06-05 12:32:57.337284+0800 Demo[16270:3721871] into 1
2017-06-05 12:32:57.337416+0800 Demo[16270:3721882] into 2
2017-06-05 12:32:58.341023+0800 Demo[16270:3721882] out 2
2017-06-05 12:32:58.341199+0800 Demo[16270:3721871] out 1
2017-06-05 12:32:58.341402+0800 Demo[16270:3721870] into 3
2017-06-05 12:32:59.345365+0800 Demo[16270:3721870] out 3
操作信號(hào)量的主要有三個(gè)函數(shù):
dispatch_semaphore_create用來(lái)創(chuàng)建信號(hào)量,參數(shù)表示初始的信號(hào)總量。
dispatch_semaphore_wait表示等待信號(hào)量,每執(zhí)行一次信號(hào)量減1,第二個(gè)參數(shù)是指等待時(shí)間,DISPATCH_TIME_FOREVER意味著永久等待,該函數(shù)會(huì)使當(dāng)前線程處于等待狀態(tài),直到以下兩種情況才會(huì)返回:
- 在等待時(shí)間內(nèi),信號(hào)量大于等于1,這時(shí)wait函數(shù)返回0,信號(hào)量減1。
- 超出了等待時(shí)間,信號(hào)量依舊不滿足大于等于1,wait函數(shù)因超時(shí)返回非0。
dispatch_semaphore_signal表示發(fā)送信號(hào)量,每執(zhí)行一次信號(hào)量加1
我們也可以使用信號(hào)量對(duì)并發(fā)線程數(shù)量進(jìn)行控制,示例如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(10);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++) {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(globalQueue, ^ {
NSLog(@"test: %d", i);
[NSThread sleepForTimeInterval:1];
dispatch_semaphore_signal(sema);
});
}
該例可以這樣理解,信號(hào)量初始為10,for循環(huán)在創(chuàng)建了10個(gè)線程后,信號(hào)量減為0,于是for循環(huán)就進(jìn)行等待,直到某一個(gè)線程結(jié)束,增加了一個(gè)新的信號(hào)量,才能繼續(xù)執(zhí)行,這樣便控制了并發(fā)數(shù)量不超過(guò)10個(gè)。
dispatch_once
該函數(shù)比較簡(jiǎn)單,常常用于單例的生成,它表示其代碼塊在程序中只會(huì)執(zhí)行一次,用法如下:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"some init...");
});
雖然使用@synchronized也能保證單例的線程安全,但dispatch_once性能要遠(yuǎn)高于@synchronized,因此單例創(chuàng)建推薦使用dispatch_once
dispatch I/O
dispatch I/O是指使用多線程進(jìn)行文件讀取的技術(shù)。主要思想是當(dāng)文件比較大時(shí),我們可以將文件分段,然后使用多線程進(jìn)行讀取,再合并,這種方式可以大大提高文件讀取速度。
dispatch source
dispatch source是指在內(nèi)核發(fā)生各種事件時(shí),開(kāi)發(fā)者可以執(zhí)行自定義處理的技術(shù)。內(nèi)核中事件的類(lèi)型有多種,最常見(jiàn)的一種是定時(shí)器事件,使用示例如下:
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue1);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"test");
});
dispatch_resume(timer);
上例創(chuàng)建了一個(gè)定時(shí)器,每?jī)擅雸?zhí)行一次block塊。dispatch_source_set_event_handler函數(shù)設(shè)置了每次定時(shí)器事件觸發(fā)時(shí)執(zhí)行的處理,通過(guò)dispatch_resume啟動(dòng)該定時(shí)器。
當(dāng)需要取消該定時(shí)器時(shí),可以通過(guò)dispatch_source_cancel取消,另外還可以指定取消時(shí)要執(zhí)行的處理。
dispatch_source_cancel(timer);
dispatch_source_set_cancel_handler(timer, ^ {
NSLog(@"cancel");
});
最后
GCD是基于內(nèi)核級(jí)別的實(shí)現(xiàn),在性能上是非常優(yōu)異的,而且它語(yǔ)法簡(jiǎn)潔,是一套非常好的多線程編程方案。