iOS多線程-GCD

1.基本介紹

什么是GCD

Grand Central Dispatch (GCD) 是異步執行任務的技術之一。開發者只需要定義想執行的任務并追加到適當的Dispatch Queue中,GCD就能生成必要的線程并計劃執行任務。

GCD的優勢

  • GCD會自動的利用多核(比如雙核,四核)
  • GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
  • GCD會自動根據系統負載來增減線程數量

Dispatch Queues

GCD的基本概念就是dispatch queue。dispatch queue是一個對象,它可以接受任務,并將任務以先到先執行的順序來執行。dispatch queue可以是并發的或串行的。并發任務會像NSOperationQueue那樣基于系統負載來合適地并發進行,串行隊列同一時間只執行單一任務。

  • 串行隊列:按FIFO每次取出一個任務執行,當前一個任務完成后才會取出第二個任務。
  • 并發隊列:按FIFO取出任務執行,但是不會等待前一個任務完成就會取出第二個任務。


    SerialQueue.png
ConcurrentQueue.png

任務

  • 同步任務:同步執行,會阻塞當前線程,直到當前的block任務執行完畢。
  • 異步任務:異步執行,不會阻塞當前線程。
隊列與任務的組合情況
同步任務 異步任務
主隊列 在主隊列添加同步任務會死鎖 不開啟新線程,在主線程按序執行任務
串行隊列 不開啟新線程,在當前線程按序執行任務 開啟一條線程,在這個線程中按序執行任務
并發隊列/全局并發隊列 不開啟新線程,在當前線程按序執行任務 GCD根據系統資源開啟多條線程執行任務

小結

  1. 同步和異步決定了是否開啟新的線程。main隊列除外,在main隊列中,同步或者異步執行都不會另開線程。
  2. 串行和并行,決定了任務是否同時執行。
  3. 不要在執行串行隊列的線程中向當前的串行隊列添加同步任務,會導致死鎖。

2.GCD使用

獲取Dispatch Queue的方法有兩種。
第一種方法是通過GCD的API生成Dispatch Queue。

dispatch_queue_t queue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);

第一個參數指定該隊列的名稱,該名稱會出現應用程序崩潰時產生的CrashLog中,在調用過程中我們也可以使用dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)獲取當前隊列的名字。

第二個參數指定該隊列的類型。

DISPATCH_QUEUE_SERIAL或者NULL表示該隊列是串行隊列,

DISPATCH_QUEUE_CONCURRENT表示該隊列是并發隊列。

第二種方法是獲取系統標準提供的Dispatch Queue。

系統會給我們提供Main Dispatch Queue(主隊列)和Global Dispatch Queue(全局并發隊列)。

Main Dispatch Queue
Main Dispatch Queue顧名思義,是在主線程中執行的隊列。因為主線程只有一個,所以主隊列自然就是串行隊列。追加到主隊列的處理在主線程的RunLoop中執行,因此要將用戶界面的更新等一些必須在主線程中執行的處理追加到主隊列中使用。

// Main 隊列獲取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });

在上面說過,同步會阻塞當前線程,執行完block里面任務才會繼續往下走。dispatch_sync阻塞了主線程,然后把任務追到加主隊列,并在主線程執行,但是此時的主線程已經被阻塞,所以block任務也無法執行,block任務不執行,dispatch_sync會繼續阻塞主線程。這樣子就產生了死鎖。

Global Dispatch Queue
Global Dispatch Queue是全局并發隊列,沒有必要通過dispatch_queue_create函數逐個生成并發,只要獲取全局并發隊列使用即可。
全局并發隊列有4個執行優先級,分別是高優先級(High Priority),默認優先級(Default Priority),低優先級(Low Priority)和后臺優先級(Background Priority)。

// 獲取高優先級
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 獲取默認優先級
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 獲取低優先級
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
// 獲取后臺優先級
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue
dispatch_queue_create函數生成的隊列不管是串行隊列還是并發隊列,都使用跟默認優先級的全局并發隊列相同的優先級。而設置一個隊列的優先級可以使用dispatch_set_target_queue。

dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serialQueue", NULL);

dispatch_queue_t backgroundGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

dispatch_set_target_queue(serialQueue, backgroundGlobalQueue);

指定要變更優先級的隊列為第一個參數,指定參考隊列為第二個參數。

dispatch_set_target_queue函數還可以改變隊列的執行層次。在多個串行隊列中,使用dispatch_set_target_queue函數指定目標為某一串行隊列,那么原本應并行執行的多個串行隊列,在目標串行隊列上只能同時執行一個任務。在必須要將不可并發執行的處理追加到多個串行隊列中時,可以使用dispatch_set_target_queue函數防止并發執行。

    dispatch_queue_t serialQueue1 = dispatch_queue_create("serialQueue1", NULL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("serialQueue2", NULL);
    dispatch_queue_t serialQueue3 = dispatch_queue_create("serialQueue3", NULL);
    dispatch_queue_t serialQueue4 = dispatch_queue_create("serialQueue4", NULL);
    
    dispatch_set_target_queue(serialQueue2, serialQueue1);
    dispatch_set_target_queue(serialQueue3, serialQueue1);
    dispatch_set_target_queue(serialQueue4, serialQueue1);
    
    dispatch_async(serialQueue2, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });
    dispatch_async(serialQueue3, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });
    dispatch_async(serialQueue4, ^{
        NSLog(@"%s",dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));
    });

    //輸出結果為
    serialQueue2
    serialQueue3
    serialQueue4

dispatch_after
想在指定時間后執行的情況,可使用dispatch_after函數來實現

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"等待3秒");
    });

使用dispatch_after需要注意的是,這個函數并不是在指定時間后執行,而是在指定時間把任務追加到指定的隊列中。如上面的代碼,3秒后只是把任務追加到主隊列當中,具體執行時間與主線程擁塞程度有關。

Dispatch Group
我們經常會有這樣的需求,在多個追加到Dispatch Queue中的處理執行完畢后,進行一些操作。在使用串行隊列的時候,我們只需要在最后追加結束后的處理。但是在使用多個并發隊列或同時使用多個隊列時,就需要使用Dispatch Group。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"block1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block3");
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });

    //執行結果
    block1
    block3
    block2
    done

很明顯,這種方式是不阻塞的。由于我們是異步把任務添加到隊列中,所以任務執行的順序是不一定的。但是dispatch_group_notify里面的block肯定是最后執行。
如果想要阻塞線程可以使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER);第二個參數為等待時間DISPATCH_TIME_FOREVER表示永遠等待。

當任務中有completion block時,這種任務是馬上完成的,例如網絡請求。但是我們想讓任務在收到completion block時才完成,這時需要我們手動管理任務的開始和結束。

    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    
    [Request requestWithSuccess:^{
        NSLog(@"success");
        dispatch_group_leave(group);
    } failBlk:^{
        NSLog(@"fail");
        dispatch_group_leave(group);
    }];
    
    
    dispatch_group_enter(group);
    
    [Request requestWithSuccess:^{
        NSLog(@"success");
        dispatch_group_leave(group);
    } failBlk:^{
        NSLog(@"fail");
        dispatch_group_leave(group);
    }];
    
    dispatch_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部完成");
    });

通過dispatch_group_enter,dispatch_group_leave兩個函數可以實現進入,退出兩個動作。要注意enterleave要成對出現,否則group永遠不會結束。

dispatch_barrier_async
在訪問數據時,使用串行隊列可以避免數據競爭問題。寫入處理不可與其他的寫入處理以及包含讀取處理的其他某些處理并行執行,但是讀取處理只和讀取處理并行執行,那么多個并行執行就不會發生問題。也就是說,為了高效率的訪問,需要實現多讀單寫。
GCD為我們提供了一種方便的實現dispatch_barrier_async,它等待所有位于barrier函數之前的操作執行完畢后執行,并且在barrier函數執行完成后,barrier函數之后的操作才會得到執行,該函數需要同dispatch_queue_create函數生成的并發隊列一起使用。

    dispatch_queue_t queue = dispatch_queue_create("queueForBarrier", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任務1");
    });
    dispatch_async(queue, ^{
        NSLog(@"任務2");
    });
    dispatch_barrier_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"任務3 barrier");
    });
    dispatch_async(queue, ^{
        NSLog(@"任務4");
    });
    dispatch_async(queue, ^{
        NSLog(@"任務5");
    });

    //輸出結果
    任務2
    任務1
    任務3 barrier
    任務4
    任務5

dispatch_apply
dispatch_apply函數按指定的次數降block追加到指定的隊列中,并阻塞線程等待全部處理執行結束。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3"];
    dispatch_apply(array.count, queue, ^(size_t index) {
        NSLog(@"%@",array[index]);
    });
    NSLog(@"完成");

    //輸出結果
    2
    1
    3
    完成

由于dispatch_apply函數會等待所有處理執行結束,所以最好在dispatch_async函數中非同步的執行dispatch_apply函數

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3"];
    dispatch_async(queue, ^{
        dispatch_apply(array.count, queue, ^(size_t index) {
            NSLog(@"%@",array[index]);
        });
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主線程
        });
    });

dispatch_suspend/dispatch_resume

    //掛起指定的隊列
    dispatch_suspend(queue);
    //恢復指定的隊列
    dispatch_resume(queue);

dispatch_suspend函數對已經執行的處理沒有影響,隊列中未執行的處理會被掛起,dispatch_resume函數會恢復這些處理的執行。

Dispatch Semaphore
信號量可以控制同時訪問資源的數量,解決資源爭奪的問題。信號量持有計數,計數為0等待,計數大于或等于1時,減去1而不等待。類似于過安檢時,允許n個人一起安檢,每當一個人安檢完成,則下一個人可以進行安檢。

    //創建計數值為1的信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSArray *array = @[@"1",@"2",@"3",@"4",@"5"];
    for (NSString *string in array) {
        dispatch_async(queue, ^{
            //dispatch_semaphore_wait函數等待信號量的計數值大于或等于1時,對計數減1并向下執行,否則等待。
            //DISPATCH_TIME_FOREVER表示永遠等待。
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

            [NSThread sleepForTimeInterval:1
            NSLog(@"%@",string);
            
            //dispatch_semaphore_signal函數將信號量的計數加1
            dispatch_semaphore_signal(semaphore);
        });
    }

上面代碼創建了一個計數初始值為1的信號量,然后向全局并發隊列中添加了5個任務,當有一個任務通過dispatch_semaphore_wait函數時,信號量的計數被減1,此時計數為0,剩余的任務則會等待,直到dispatch_semaphore_signal將計數加1。

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    long result = dispatch_semaphore_wait(semaphore, time);
    if (result == 0) {
        //在等待時間內計數值達到大于等于1,減1繼續處理
    }
    else{
        //在等待時間內計數值為0
    }

我們也可以通過dispatch_semaphore_wait函數的返回值進行分支處理。當我們給定一個等待時間,如果信號量的計數值在等待時間內達到大于等于1,dispatch_semaphore_wait返回0,如果超過這個時間計數值還為0,則返回值不為0。

dispatch_once
dispatch_once函數保證在應用程序執行中只執行一次處理。并且在多線程環境下能保證安全。

@implementation Manager

+ (Manager *)sharedInstance
{
    static Manage *manager = nil;
    static dispatch_once_t token;

    dispatch_once(&token, ^{
        manager = [[Manager alloc] init];
    });

    return manager;
}

我們一般用來創建單例。

Dispatch Source
Dispatch Source的種類

名稱 種類
DISPATCH_SOURCE_TYPE_DATA_ADD 自定義事件,變量增加
DISPATCH_SOURCE_TYPE_DATA_OR 自定義事件,變量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH 端口發送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH 端口接收
DISPATCH_SOURCE_TYPE_PROC 檢測到與進程相關的事件
DISPATCH_SOURCE_TYPE_READ IO操作,如對文件的操作、socket操作的讀響應
DISPATCH_SOURCE_TYPE_SIGNAL 接受信號
DISPATCH_SOURCE_TYPE_TIMER 定時器
DISPATCH_SOURCE_TYPE_VNODE 文件系統有變更
DISPATCH_SOURCE_TYPE_WRITE IO操作,如對文件的操作、socket操作的寫響應

使用DISPATCH_SOURCE_TYPE_TIMER的定時器的例子

    //倒計時時間
    __block int timeout = 60;
    
    //創建隊列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    //創建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    //設置開始時間未當前時間,間隔1秒,誤差為0
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC), 0);
    
    //觸發事件
    dispatch_source_set_event_handler(timer, ^{
        if (timeout <= 0) {
            //取消dispatch source
            dispatch_source_cancel(timer);
        }
        else{
            timeout --;
            NSLog(@"%d",timeout);
        }
    });
    
    //啟動定時器
    dispatch_resume(timer);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • 一、簡介在iOS所有實現多線程的方案中,GCD應該是最有魅力的,因為GCD本身是蘋果公司為多核的并行運算提出的解決...
    MYS_iOS_8801閱讀 576評論 0 0
  • 多線程學習筆記-GCD 我把這篇文章所用到的代碼總結到這里->GCD項目總結下載地址-GCD-wxk可以下載參考 ...
    wxkkkkk閱讀 541評論 0 2
  • 目錄:iOS多線程(一)--pthread、NSThreadiOS多線程(二)--GCD詳解iOS多線程(三)--...
    Claire_wu閱讀 1,086評論 0 6
  • 一、基本概念 線程是用來執行任務的,線程徹底執行完任務A才能執行任務B,為了同時執行兩個任務,產生了多線程 1、進...
    空白Null閱讀 703評論 0 3
  • 為了能更好的傳播產品經理精品文章,我們特意創建「PM 周刊」,將通過微信和郵件的形式推送給大家,每周周一定時推送。...
    四勾4J閱讀 188評論 0 0