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