iOS底層探索:多線程與GCD

一、多線程

1.1進程與線程

進程:進程是指在系統中正在運行的一個應用程序;每個進程之間是獨立的,每個進程均運行在其專用的受保護的內存空間內。

線程:線程是進程的基本執行單元,一個進程的所有任務都在線程中執行;進程要想執行任務,必須要有線程,進程至少要有一條線程;程序啓動回默認開啓一條線程,即主線程。

多線程原理:同一時間單核CPU只能處理一個線程,即只有一個線程在執行。多線程同時執行是CPU快速的在多個線程之間切換,CPU調度線程的時間足夠快,就造成了多線程同時執行的效果。如果線程非常多,CPU會在N個線程之間切換,消耗大量的CPU資源,每個線程被調度的次數會降低,線程的執行效率也會降低。

多線程技術方案

方案 說明 語言 生命週期 使用頻率
pthread 一套通用的多線程API;適用於Unix/Linux/Windows等平臺;跨平臺,可移植;適用難度大 c 開發管理 很低
NSThread 使用更加面向對象;簡單易用,可直接操作線程對象 OC 開發管理
GCD 旨在替代NSThread技術,充分利用設備的多核 c 自動管理
NSOperation 基於GCD(底層是GCD);比GCD多了一些更簡單實用的功能;實用更加面向對象 OC 自動管理

1.2 任務

任務是指執行的操作,簡單說就是在縣城中執行的那段代碼。在GCD中是放在block中的。

任務的執行有兩種方式:同步執行和異步執行。兩者的區別主要在於是否等待隊列中的任務執行完畢,以及是否具備開啓新線程的能力。

  • 同步執行(sync):同步添加任務到指定的隊列中,在添加的任務執行結束前會一直等待,直到隊列裏的任務完成後再繼續執行;只能在當前線程執行任務,不具備開啓新線程的能力。加入方式dispatch_sync
  • 異步執行(async):異步添加任務到指定隊列,不會做任何等待,可以繼續執行任務;可以在新的線程執行任務,具備開啓新線程的能力,但是並不一定開啓新線程,這跟任務所在的隊列有關。加入方式dispatch_async

任務執行速度的影響因素:1.CPU。2.線程狀態。3.任務的複雜度。4.任務的優先級。其中任務的優先級包括用戶指定的qualityServiceuserInteractiveuserInitiatedutilitybackgrounddefault);等待的頻繁程度(不執行)。

二、GCD

隊列(Dispatch Queue)指的是執行任務的等待隊列,即用來存放任務的隊列。隊列採用FIFO(先進先出)的原則,新任務總是被插入到隊列的末尾,讀取任務的時候總是從隊列的頭部開始讀取,每讀取一個任務,則從隊列中釋放一個任務。

GCD中有2種隊列:串行隊列和併發隊列,兩者均遵循FIFO的原則,不同點在於執行的順序以及開啓線程的數量。

  • 串行隊列(Serial Dispatch Queue):只開啓一個線程,一個任務執行完畢纔會執行下一個任務。每次只有一個任務被執行。
  • 併發隊列(Concurrent Dispatch Queue):可以開啓多個線程,並且同時執行任務。可以讓多個任務同時執行。

隊列的創建:可以使用dispatch_queue_create創建,該方法需要傳入2個參數:第一個參數表示隊列的標識,可爲空;第二個參數用來識別是串行隊列還是兵法隊列,DISPATCH_QUEUE_SERIAL(==NULL)標識串行隊列,DISPATCH_QUEUE_CONCURRENT

//串行隊列
dispatch_queue_t s = dispatch_queue_create("com.appex.queue.serial", DISPATCH_QUEUE_SERIAL);
//併發隊列
dispatch_queue_t c = dispatch_queue_create("com.appex.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);

主隊列:主隊列(Main Dispatch Queue)是一種特殊的串行隊列,說他特殊是因爲默認情況下代碼就在主隊列中,主隊列的代碼又都會放在主線程中執行。獲取方式:

//獲取主隊列
dispatch_group_t main = dispatch_get_main_queue();

全局併發隊列:全局併發隊列是(Global Dispatch Queue)是系統提供的併發隊列,獲取方法:

//獲取全局併發隊列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//第一個參數表示優先級的高低,一般傳`DISPATCH_QUEUE_PRIORITY_DEFAULT`
DISPATCH_QUEUE_PRIORITY_HIGH       
DISPATCH_QUEUE_PRIORITY_DEFAULT    
DISPATCH_QUEUE_PRIORITY_LOW      
DISPATCH_QUEUE_PRIORITY_BACKGROUND 
//第二個參數暫時沒用,傳0即可。

如果當前在主線程,按照隊列的串行和併發,任務的同步和異步特性組合,我們歸納如下:

區別 併發隊列 串行隊列 主隊列
同步 沒有開啓新線程,串行執行任務 沒有開啓新線程,串行執行任務 死鎖
異步 有開啓新線程,併發執行任務 有開啓新線程(1條),串行執行任務 沒有開啓新線程,串行執行任務
/*死鎖案例-1*/
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    /*async沒有開啓新線程,以下代碼在主線程中運行*/
    NSLog(@"開始:%@", [NSThread currentThread]);
    dispatch_sync(queue, ^{
         NSLog(@"sync:%@", [NSThread currentThread]);
     });
     NSLog(@"結束:%@", [NSThread currentThread]);
});

在主線程中,向主隊列添加同步任務會死鎖。這是因爲添加的任務和主隊列自身的任務相互等待,阻塞了主隊列,最終造成主隊列所載的線程(主線程)死鎖。如果在其他線程向主隊列添加同步任務,則不會死鎖。

/*死鎖案例-2*/
dispatch_queue_t queue = dispatch_queue_create("com.app.serial", 0);
dispatch_async(queue, ^{
    /*async開啓1條新線程,以下代碼在子線程中運行*/
    NSLog(@"開始:%@", [NSThread currentThread]);
    dispatch_sync(queue, ^{
         NSLog(@"sync:%@", [NSThread currentThread]);
   });
   NSLog(@"結束:%@", [NSThread currentThread]);
});

在子線程中,向串行隊列添加同步任務會死鎖。這是因爲添加的任務和串行隊列自身的任務相互等待,阻塞了串行隊列,最終造成串行隊列所載的線程(子線程)死鎖。如果在其他線程向該隊列添加同步任務,則不會死鎖。

以上案例可以概括爲在一個串行隊列所載的線程,向該隊列添加同步任務會造成串行隊列追加的任務和原有的任務相互等待而阻塞當前線程。

三、GCD其他常用函數

3.1 dispatch_after:表示在某個隊列中異步延遲執行任務

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t work){
    _dispatch_after(when, queue, NULL, work, true);
}

第一個參數when表示開始的時間,通常在現在的時間時間的基礎上加時間,如dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)表示2秒之後;第二個參數queue傳入隊列,第三個參數work傳入任務的block代碼。
常規用法如下:

NSLog(@"開始:%@", [NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2), dispatch_get_main_queue(), ^{
   NSLog(@"after:%@", [NSThread currentThread]);
});

3.2 dispatch_once:代碼只執行一次

void dispatch_once(dispatch_once_t *val, dispatch_block_t block){
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

第一個參數val傳入一個dispatch_once_t類型的指針地址,第二個參數block傳入只執行一次的block任務。
常規用法:

@interface NXDownloader : NSObject
+ (NXDownloader *)downloader;
@end

@implementation NXDownloader
+ (NXDownloader *)downloader{
    static dispatch_once_t t;
    static NXDownloader *sharedInstance;
    dispatch_once(&t, ^{
        sharedInstance = [[NXDownloader alloc] init];
    });
    return sharedInstance;
}
@end

這樣我們通過NXDownloader *downloader = [NXDownloader downloader];獲取到的實例都是同一個,而且是線程安全的。

3.3dispatch_barrier_async/dispatch_barrier_sync:柵欄函數

柵欄函數的作用就是隔離柵欄函數之前與之後的代碼執行,只有前面的代碼執行完畢纔會執行後面的代碼,函數如下:

void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work){
    ...
}

void dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work){
    ...
}

柵欄函數的第一個參數dq接收一個隊列,按照代碼註釋的說明,這裏需要傳入一個併發隊列纔會真正的發揮柵欄函數的功能,如果傳入的dq是個串行隊列,則函數的表現與dispatch_barrier_asyncdispatch_barrier_sync表現一樣。

那麼這裏的asyncsync的作用有什麼不同呢?看如下代碼:

dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"1:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"2:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"3:%@", [NSThread currentThread]);
});
NSLog(@"0-0:%@",[NSThread currentThread]);

//dispatch_barrier_async或dispatch_barrier_sync
dispatch_barrier_async(queue, ^{
    sleep(2);
    NSLog(@"barrier:%@", [NSThread currentThread]);
});

NSLog(@"0-1:%@",[NSThread currentThread]);
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"4:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"5:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"6:%@", [NSThread currentThread]);
});

多次打印,結果如下:


執行結果顯示:{1,2,3}執行的順序是不確定,{4,5,6}執行順序也是不確定的,可以確定的是{1,2,3}執行完畢後執行barrier,再執行{4,5,6}。在async的情況下0-00-1最先執行。而sync的情況下0-0在barrier之前執行,0-1barrier之後執行。也就是sync會如同dispatch_sync一樣執行完畢後仔執行後面的代碼。並且在sync的情況下barrier任務會在原有的線程(這裏是主線程)中執行。

3.4dispatch_group,隊列組

隊列組簡言之就是一組任務執行完畢後會有一個單獨的回調。

//隊列組的創建
dispatch_group_t group = dispatch_group_create();
//添加任務
    dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
    NSLog(@"執行");
});
//任務執行完畢的回掉
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
      NSLog(@"結束:%@");
});

其中dispatch_group_async會把任務放入隊列,再把隊列放入隊列組。也可以用dispatch_group_enterdispatch_group_leave成對使用。 dispatch_group_notify會在任務執行完畢後回調,你可以指定一個隊列繼續做其他事情。還有一個不太常用的dispatch_group_wait函數,這個函數第一個參數傳入一個group,第二個參數傳入一個time,這個時間指定的是dispatch_group_wait之後的代碼等待的最大時間,假定這裏設定的時間是3秒,如果前面的任務2秒執行完畢,那麼wait後面的代碼會在2秒後執行。如果前面的任務4秒執行完畢,那麼wait後面的代碼會在第3秒的時候開始執行。

dispatch_group_notify案例

//dispatch_group_notify案例:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

for (int i= 0; i < 10; i++){
    dispatch_group_async(group, queue, ^{
        NSLog(@"%d:%@", i, [NSThread currentThread]);
    });

    /*以上三行代碼等價於
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
         NSLog(@"%d:%@", i, [NSThread currentThread]);
         dispatch_group_leave(group);
    });
    */
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"結束:%@", [NSThread currentThread]);
});

dispatch_group_wait案例

//dispatch_group_wait案例
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

for (int i= 0; i < 10; i++){
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"%d:%@", i, [NSThread currentThread]);
    });
}
//dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC*2)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"結束:%@", [NSThread currentThread]);

這個場景在實際開發中使用較多,比如現在有一組網絡圖片需要分享到某個平臺,我們需要等待多張網絡圖片全鼻下載完畢後才能開始分享。

到此多線程以及GCD相關的問題已經梳理的差不多了,如有錯誤,歡迎指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章