Objective-C高級編程之GCD篇

多線程編程是一項非常重要的技術(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)前線程停止,等待直到:

  1. 在等待時間內(nèi),group中所有任務(wù)執(zhí)行完畢,返回0
  2. 在等待時間內(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),直到以下兩種情況才會返回:

  1. 在等待時間內(nèi),信號量大于等于1,這時wait函數(shù)返回0,信號量減1。
  2. 超出了等待時間,信號量依舊不滿足大于等于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)異的,而且它語法簡潔,是一套非常好的多線程編程方案。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容