GCD

GCD(Grand Central Dispatch)是異步執(zhí)行任務(wù)的技術(shù)之一。一般將應(yīng)用程序中記述的線程管理用的代碼在系統(tǒng)級中實現(xiàn)。開發(fā)者只需要定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)谼ispatch Queue中,GCD就能生成必要的線程并計劃執(zhí)行任務(wù)。由于線程管理是作為系統(tǒng)的一部分來實現(xiàn)的,因此可統(tǒng)一管理,也可執(zhí)行任務(wù),這樣就比以前的線程更有效率。

dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    /**
     *  長時間處理
     *  例如:AR用圖像識別、數(shù)據(jù)庫訪問
     */
    
    /**
     *  長時間處理結(jié)束,主線程使用該處理結(jié)果
     */
    
    dispatch_async(dispatch_get_main_queue(), ^{
        /**
         *  只在主線程可以執(zhí)行的處理
         *  例如用戶界面刷新
         */
    });
});

在導(dǎo)入GCD之前,Cocoa框架提供了NSObject類的performSelectorInBackground:withObject實例方法和performSelectorOnMainThread實例方法等簡單的多線程編程技術(shù)。

線程

線程是程序中一個單一的順序控制流程。進(jìn)程內(nèi)一個相對獨立的、可調(diào)度的執(zhí)行單元,是系統(tǒng)獨立調(diào)度和分派CPU的基本單位指運行中的程序的調(diào)度單位。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。
“一個CPU執(zhí)行的CPU命令列為一條無分叉路徑”即為“線程”。
現(xiàn)在一個物理的CPU芯片實際上有64個(64核)CPU,盡管如此,“一個CPU執(zhí)行的CPU命令列為一條無分叉路徑”仍然不變。
    
OS X和iOS的核心XNU內(nèi)核再愛發(fā)生操作系統(tǒng)事件時(如每隔一定時間,喚起系統(tǒng)調(diào)用等情況)會切換執(zhí)行路徑。執(zhí)行中路徑的狀態(tài),例如CPU的寄存器等信息保存到各自路徑專用的內(nèi)存塊中,從切換目標(biāo)路徑專用的內(nèi)存塊中,復(fù)原CPU寄存器等信息,繼續(xù)執(zhí)行切換路徑的CPU命令列。這稱為“上下文切換”。
由于使用多線程的程序可以在某個線程和其他線程之間反復(fù)多次進(jìn)行上下文切換,因此看上去好像1個CPU核能夠并列地執(zhí)行多個線程一樣。而且在具有多個CPU核的情況下,就不是“看上去像”了,而是真的提供了多個CPU核并行執(zhí)行多個線程的技術(shù)。

使用多線程容易引發(fā)的常見問題

  • 數(shù)據(jù)競爭(多個線程更新相同的資源會導(dǎo)致數(shù)據(jù)不一致)
  • 死鎖(停止等待事件的線程會導(dǎo)致多個線程相互持續(xù)等待)
  • 內(nèi)存占用(使用太多線程會消耗大量內(nèi)存)

盡管容易發(fā)生問題,但是為了保證應(yīng)用程序的響應(yīng)性能,也應(yīng)當(dāng)使用多線程編程。

GCD的API

Dispatch Queue

Dispatch Queue是執(zhí)行處理的等待隊列,按照FIFO(先進(jìn)先出)的追加順序執(zhí)行處理。開發(fā)者要做的只是定義想執(zhí)行的任務(wù)并追加到適當(dāng)?shù)谼ispatch Queue中。
Dispatch Queue分兩種:1.等待現(xiàn)在執(zhí)行中處理結(jié)束的Serial Dispatch Queue;2.不等帶現(xiàn)在執(zhí)行中處理結(jié)束的Concurrent Dispatch Queue。

dispatch_queue_create

生成Dispatch Queue的方法。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MyQueue", DISPATCH_QUEUE_CONCURRENT);

/* dispatch_release(queue); */

如果你部署的最低目標(biāo)低于 iOS 6.0 or Mac OS X 10.8,你應(yīng)該自己管理GCD對象,使用(dispatch_retain,dispatch_release),ARC并不會去管理它們。
如果你部署的最低目標(biāo)是 iOS 6.0 or Mac OS X 10.8 或者更高的版本,
ARC已經(jīng)能夠管理GCD對象了,這時候,GCD對象就如同普通的OC對象一樣,不應(yīng)該使用dispatch_retain 或者 dispatch_release

為了避免多個線程更新相同資源導(dǎo)致數(shù)據(jù)競爭,推薦使用Serial Dispatch Queue。
當(dāng)想并發(fā)執(zhí)行不發(fā)生數(shù)據(jù)競爭等問題的處理時,使用Concurrent Dispatch Queue。

Main Dispatch Queue / Global Dispatch Queue

系統(tǒng)提供的Dispatch Queue。

Main Dispatch Queue是在主線程中執(zhí)行的Dispatch Queue。
因為主線程只有1個,所以它是Serial Dispatch Queue。
追加到Main Dispatch Queue的處理在主線程的Runloop中執(zhí)行。

Global Dispatch Queue是所有應(yīng)用程序都能夠使用的Concurrent Dispatch Queue。沒有必要通過dispatch_queue_create函數(shù)逐個生成Concurrent Dispatch Queue。只要獲取Global Dispatch Queue使用即可。

表 1-1 Dispatch Queue種類

名稱 Dispatch Queue的種類 說明
Main Dispatch Queue Serial Dispatch Queue 主線程執(zhí)行
Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 執(zhí)行優(yōu)先級:高(最高優(yōu)先級)
Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 執(zhí)行優(yōu)先級:默認(rèn)
Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 執(zhí)行優(yōu)先級:低
Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 執(zhí)行優(yōu)先級:后臺

/* 獲取方法:*/

dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue(); dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_set_target_queue

dispatch_queue_create函數(shù)生成的Dispatch Queue不管是Serial Dispatch Queue還是Concurrent Dispatch Queue,都使用與默認(rèn)優(yōu)先級Global Dispatch Queue相同執(zhí)行優(yōu)先級的線程。而變更生成的Dispatch Queue的執(zhí)行優(yōu)先級要使用dispatch_set_target_queue函數(shù)。

/* code */
dispatch_queue_t mySerialDispatchQueue =        dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);

dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

/* end */

指定要變更執(zhí)行優(yōu)先級的Dispatch Queue為dispatch_set_target_queue函數(shù)的第一個參數(shù),制定與要使用的執(zhí)行優(yōu)先級相同優(yōu)先級的Global Dispatch Queue為第二個參數(shù)(目標(biāo)優(yōu)先級)。

dispatch_after

當(dāng)我們想要在指定時間后執(zhí)行某個處理時(切確來說,是在指定時間追加處理到Dispatch Queue),使用dispatch_after函數(shù)。

/* 在3秒后用dispatch_asyn函數(shù)追加Block到Main Dispatch Queue 
 * ull 是C語言的數(shù)值字面量,是顯示表明類型時使用的字符串(表示“unsigned long long”)
 */
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
   NSLog(@"waited at least three seconds.");
});

第一個參數(shù)是指定時間用的dispatch_time_t類型的值。可以使用dispatch_time函數(shù)或者dispatch_walltime函數(shù)獲得。
dispatch_time函數(shù)能夠獲取從第一個參數(shù)dispatch_time_t類型值中指定的時間開始,到第二個參數(shù)指定的毫微秒單位時間后的時間。(相對時間)
dispatch_walltime函數(shù)通常用于計算絕對時間,比如:2011年11月11日11分11秒這一絕對時間,這可以當(dāng)做粗略的鬧鐘功能使用。dispatch_walltime函數(shù)由POSIX中使用的struct timespec類型的時間得到dispatch_time_t類型的值。

Dispatch Group

在追加到Dispatch Queue中的多個處理全部結(jié)束后想執(zhí)行結(jié)束處理,這種情況會經(jīng)常出現(xiàn)。只使用一個Serial Dispatch Queue時,只要將想執(zhí)行的處理全部追加到該Serial Dispatch Queue中并在最后追加結(jié)束處理,即可實線。但是在使用Concurrent Dispatch Queue時或同時使用多個Dispatch Queue時,源代碼就會變得頗為復(fù)雜。這是就用到Dispatch Group。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk2");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"blk3");
});

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"done");
});

這里除了使用dispatch_group_notify,還可以使用dispatch_group_wait函數(shù)。

    例如: 
    long result = dispatch_group_wait(group,DISPATCH_TIME_FOREVER);
    永遠(yuǎn)等待下去,直到全部處理完成,所以result恒為0
    
    例如:
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    long result = dispatch_group_wait(group, time);

    if (result == 0) {
        // 屬于Dispatch Group的全部處理結(jié)束
    } else {
        // 屬于Dispatch Group的某個處理還在執(zhí)行中
    }

如果dispatch_group_wait函數(shù)的返回值不為0,就意味著雖然經(jīng)過了指定的時間,
但屬于Dispatch Group的某一個處理還在執(zhí)行中。
如果返回值為0,那么全部處理執(zhí)行結(jié)束。指定DISPATCH_TIME_NOW,則不用任何等待即可判定屬于Dispatch Group的處理
是否執(zhí)行結(jié)束。

long result = dispatch_group_wait(group,DISPATCH_TIME_NOW);

在主線程的Runloop的每次循環(huán)中,可檢查執(zhí)行是否結(jié)束,從而不耗費多余的等待時間,雖然這樣可以,但一般在這種情況下,還是推薦用dispatch_group_notify函數(shù)追加技術(shù)處理到Main Dispatch Queue中,這樣可以簡化源代碼。

dispatch_barrier_async

在訪問數(shù)據(jù)庫或文件時,使用Serial Dispatch Queue可避免數(shù)據(jù)競爭的問題。
寫入處理確實不可與其他的處理以及包含讀取處理的其他處理并行執(zhí)行。但是如果讀取處理只是與讀取處理并行執(zhí)行,那么多個并行執(zhí)行就不會發(fā)生問題。也就是說,為了高效率地進(jìn)行訪問,讀取處理追加到Concurrent Dispatch Queue中,寫入處理在任何一個讀取處理沒有執(zhí)行的狀態(tài)下,追加到Serial Dispatch Queue中即可(在寫入處理結(jié)束之前,讀取處理不可執(zhí)行)。

這時,用到dispatch_barrier_async函數(shù)。dispatch_barrier_async函數(shù)會等待追加到Concurrent Dispatch Queue上的并行執(zhí)行的處理全部結(jié)束之后,再將指定的處理追加到該Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函數(shù)追加的處理執(zhí)行完畢后,Concurrent Dispatch Queue才恢復(fù)為一般的動作,追加到該Concurrent Dispatch Queue的處理又開始并行執(zhí)行。

dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
/* 寫入處理 */
dispatch_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);

dispatch_sync(同步)
dispatch_apply(同步操作)

dispatch_apply函數(shù)是dispatch_sync函數(shù)和Dispatch Group的關(guān)聯(lián)API。該函數(shù)按指定的次數(shù)將指定的Block追加到指定的Dispatch Queue中,并等待全部處理執(zhí)行結(jié)束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zu",index);
});

dispatch_suspend / dispatch_resume

當(dāng)追加大量處理到Dispatch Queue時,在追加處理的過程中,有時希望不執(zhí)行已經(jīng)追加的處理。這時,需要掛起Dispatch Queue,當(dāng)可以執(zhí)行時再恢復(fù)。

dispatch_suspend函數(shù)掛起指定的Dispatch Queue。

dispatch_suspend(queue);

dispatch_resume函數(shù)恢復(fù)指定的Dispatch Queue。

dispatch_resume(queue);

這兩個函數(shù)對已經(jīng)執(zhí)行的處理沒有影響。掛起后,追加到Dispatch Queue中但尚未執(zhí)行的處理在此之后停止執(zhí)行。而恢復(fù)則使得這些處理能夠繼續(xù)執(zhí)行。

Dispatch Semaphore

如前所述,當(dāng)并行執(zhí)行的處理更新數(shù)據(jù)時,會產(chǎn)生數(shù)據(jù)不一致的情況,有時應(yīng)用程序還會異常結(jié)束。雖然使用Serial Dispatch Queue和dispatch_barriel_asyn函數(shù)可以避免這類問題,但有必要進(jìn)行更細(xì)粒度的排他控制。

Dispatch Semaphore是持有計數(shù)的信號,該計數(shù)是多線程編程中的計數(shù)類型信號。計數(shù)為0時等待,計數(shù)為1或者大于1時,減去1而不等待。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    /**
     *  生成Dispatch Semaphore
     *
     *  Dispatch Semaphore的計數(shù)初始值設(shè)定為“1”。
     *
     *  保證可訪問NSMutableArray類對象的線程同時只能有一個
     *
     */
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *array = [NSMutableArray array];

    for (NSInteger i = 0; i < 100000; i++) {
        dispatch_async(queue, ^{
            /**
             * 等待Dispatch Semaphore
             * 一直等待,直到Dispatch Semaphore的計數(shù)值大于或者等于 1
             */
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            /**
             * 由于Dispatch Semaphore的計數(shù)值大于等于1
             * 所以將Dispatch Semaphore的計數(shù)值減去1
             * dispatch_semaphore_wait函數(shù)執(zhí)行返回
             *
             * 執(zhí)行到此時,計數(shù)值恒為“0”
             *
             * 此時可訪問NSMutableArray類對象的線程只有1個
             * 可以安全地進(jìn)行更新
             */
            
            [array addObject:[NSNumber numberWithInt:1]];
            
            /**
             *  排他控制結(jié)束
             *  使用dispatch_semaphore_signal函數(shù)將計數(shù)值加 1
             *
             *  如果有通過dispatch_semaphore_wait函數(shù)等待計數(shù)值增加的線程
             *  由最先等待的線程執(zhí)行。
             */
            
            dispatch_semaphore_signal(semaphore);
        });
    }

dispatch_once

dispatch_once函數(shù)是保證在應(yīng)用程序執(zhí)行中只執(zhí)行一次指定處理的API。在生成單例對象時使用。在多線程下執(zhí)行,也可保證百分之百安全。

static dispatch_once_t pred;
dispatch_once(&pred, ^{
    /**
     *  初始化
     */
});

Dispatch I/O

在讀取較大文件時,如果將文件分成合適的大小使用Global Dispatch Queue并列讀取的話,會比一般的讀取速度快不少。
蘋果中使用Dispatch I/O 和 Dispatch Data的例子

pipe_q = dispatch_queue_create("PipeQ", NULL);
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
    close(fd);
});

*out_fd = fdpair[1];

dispatch_io_set_low_water(pipe_channel, SIZE_MAX);

dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
    if (err == 0)
    {
        size_t len = dispatch_data_get_size(pipedata);
        if (len > 0)
        {
            const char *bytes = NULL;
            char *encoded;
            dispatch_data_t md = dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
            encoded = asl_core_encode_buffer(bytes, len);
            asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
            free(encoded);
            _asl_send_message(NULL, merged_msg, -1, NULL);
            asl_msg_release(merged_msg);
            dispatch_release(md);
        }
    }
    
    if (done)
    {
        dispatch_semaphore_signal(sem);
        dispatch_release(pipe_channel);  
        dispatch_release(pipe_q);  
    }  
});

以上摘自Apple System Log API用的源代碼(Libc-763.11 gen/asl.c)。dispatch_io_create函數(shù)創(chuàng)建了一個dispatch I/O。并指定發(fā)生錯誤的時候被執(zhí)行的block,以及執(zhí)行該block的隊列。dispatch_io_set_low_water函數(shù)設(shè)定一次讀取的大小(分割大小),dispatch_io_read函數(shù)在全局隊列上開啟讀取操作。每當(dāng)一塊數(shù)據(jù)被讀取后,數(shù)據(jù)作為參數(shù)會被傳遞給dispatch_io_read函數(shù)指定的讀取結(jié)束時回調(diào)的Block。回調(diào)的用的Block分析傳遞過來的Dispatch Data并進(jìn)行結(jié)合處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 我們知道在iOS開發(fā)中,一共有四種多線程技術(shù):pthread,NSThread,GCD,NSOperation: ...
    請叫我周小帥閱讀 1,507評論 0 1
  • 章節(jié)目錄 什么是GCD? 如何在多條路徑中執(zhí)行CPU命令列? 即使多線程存在很多問題(如數(shù)據(jù)競爭、死鎖、線程過多消...
    DrunkenMouse閱讀 883評論 1 13
  • 簡介 GCD(Grand Central Dispatch)是在macOS10.6提出來的,后來在iOS4.0被引...
    sunmumu1222閱讀 894評論 0 2
  • 一、GCD的API 1. Dispatch queue 在執(zhí)行處理時存在兩種Dispatch queue: 等待現(xiàn)...
    doudo閱讀 508評論 0 0
  • 2016.8.11-2016.8.21 晴 說是11日,其實算下來,也就8日,其他時間都是在趕車趕飛機。 途徑曼谷...
    艾力紳閱讀 206評論 0 0