iOS多線程之2.GCD

??GCD(Grand Central Dispatch)是蘋果公司專門為多核并發運算提出的解決方案,是純C語言的,提供了很多非常強大的函數。本文主要介紹串行隊列和并行隊列以及同步和異步往隊列里添加任務的區別。

1.GCD的優勢

??(1)會自動利用更多的CPU內核(從iPhone4s開始是雙核,iPhone7系列是4核)。
??(2)會自動管理線程的生命周期(創建、銷毀)等,不需要寫一句關于線程的代碼。
??(3)將任務添加到隊列中,GCD會自動從隊列中將任務取出,放到對應的線程中執行。

2.隊列(queue)

??GCD相較于NSThread最大的改變就是:NSThread是直接把任務放在線程里,而GCD是把任務放在隊列里。隊列就是盛放任務的容器。GCD會根據CPU的使用情況自己創建線程,執行隊列里的任務。隊列里任務的調度遵行FIFO原則,就是哪個任務先放進隊列中,就先取出來執行。
??隊列分為串行隊列,和并行(并發)隊列。
??串行隊列 : 就是隊列里的任務前一個執行完,后一個才開始執行。串行隊列里異步添加的任務都在一個線程里執行。
??創建串行隊列有兩種方法:
(1)自定義串行隊列

     // 創建串行隊列
    //  doujiangyoutiao 隊列的名字
    //  DISPATCH_QUEUE_SERIAL 表示創建的是串行隊列
    dispatch_queue_t queue = dispatch_queue_create("doujiangyoutiao", DISPATCH_QUEUE_SERIAL);
    // 添加任務A
    dispatch_async(queue, ^{
        NSLog(@"執行任務A");
    });
    // 添加任務B
    dispatch_async(queue, ^{
        NSLog(@"執行任務B");
    });
    // 添加任務C
    dispatch_async(queue, ^{
        NSLog(@"執行任務C");
    });

日志

執行任務A
執行任務B
執行任務C

(2)主隊列

// 得到主隊列
dispatch_queue_t queue =dispatch_get_main_queue();

??主隊列是串行隊列,主隊列中的任務默認都在主線程中執行。
??并行隊列:就是后面添加的任務不必等后面的任務執行完再開始執行,有可能一起開始執行。所以并行任務里第一個添加的任務并不一定第一個執行完。
??創建并行隊列也有兩種方法:
(1)和創建串行隊列、添加任務的方式一樣。

// 創建并行隊列
//  豆丶漿油條 隊列的名字
//  DISPATCH_QUEUE_CONCURRENT 表示創建的是并行隊列
dispatch_queue_t queue = dispatch_queue_create("豆丶漿油條",DISPATCH_QUEUE_CONCURRENT);

(2)獲取系統的全局并發隊列。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //  獲取系統的全局并發隊列
   // DISPATCH_QUEUE_PRIORITY_DEFAULT表示隊列的優先級一共三個值:DISPATCH_QUEUE_PRIORITY_HIGH、DISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW、DISPATCH_QUEUE_PRIORITY_BACKGROUND
 // 第二個參數是預留參數,不起作用,傳0就行
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 添加任務A
    dispatch_async(queue, ^{
        NSLog(@"執行任務A");
    });
    // 添加任務B
    dispatch_async(queue, ^{
        NSLog(@"執行任務B");
    });
    // 添加任務C
    dispatch_async(queue, ^{
        NSLog(@"執行任務C");
    });
}

??每次打印的日志應該都不一樣,有興趣可以多試幾次。并行隊列里的任務執行完的順序是不固定的。

執行任務B
執行任務C
執行任務A

注意

  1. 并發隊列里的任務一定會一起執行嘛?
    答:不一定。如果并發隊列里有100個任務,一起執行就得創建100條線程,那不得累死CPU啊!任務少了有可能會一起執行,任務多了會先一起執行一部分。
  2. 線程并發???
    答:錯。串行并發是形容隊列的!??!線程沒有,線程里的任務都是一個一個的往下執行,不可能多個任務一起執行。

3.同步和異步

??同步和異步是指向隊列里添加任務的方式,上文往線程里都是異步添加任務的。

- (void)viewDidLoad {
    [super viewDidLoad];
 
   dispatch_queue_t queue = dispatch_queue_create("豆丶漿油條", DISPATCH_QUEUE_CONCURRENT);
    // 異步向隊列里添加任務
    dispatch_async(queue, ^{
        NSLog(@"任務1------%@",[NSThread currentThread]);
    });
    
    // 同步向隊列里添加任務
    dispatch_sync(queue, ^{
        sleep(1);
        NSLog(@"任務2------%@",[NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"任務3------%@",[NSThread currentThread]);
    });
}

日志

任務1------<NSThread: 0x2805aa5c0>{number = 3, name = (null)}
任務2------<NSThread: 0x2805f6e80>{number = 1, name = main}
任務3------<NSThread: 0x2805aa5c0>{number = 3, name = (null)}

??由于同步任務都是在當前線程執行的,并且它執行完以后,后面的任務才能執行,所以會堵塞當前線程。例子中由于任務2是在主線程里添加的,盡管執行了掛起1秒,但是后面的任務3也要等任務2執行完以后再執行,這樣就堵塞了主線程。所以盡量不要在主線程中添加同步任務,會影響用戶交互
??異步添加任務不會堵塞當前線程,也不會影響后面任務的執行。所以我們盡量使用異步向隊列中添加任務。

4. 用GCD實現多線程并發

??用GCD實現多線程并發必須滿足兩個條件:并發隊列;異步向隊列里添加任務。

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // 獲取全局并發隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 可以自己創建并發隊列
//    dispatch_queue_t queue = dispatch_queue_create("豆漿油條", DISPATCH_QUEUE_CONCURRENT);
   
    // 異步添加任務1
    dispatch_async(queue, ^{
        NSLog(@"任務1開始執行");
        sleep(1);
        NSLog(@"任務1執行完了");
    });
    
    // 異步添加任務2
    dispatch_async(queue, ^{
        NSLog(@"任務2開始執行");
        sleep(2);
        NSLog(@"任務2執行完了");
    });
    
    // 異步添加任務3
    dispatch_async(queue, ^{
        NSLog(@"任務3開始執行");
        sleep(3);
        NSLog(@"任務3執行完了");
    });
}

日志

2019-04-20 23:27:54.081762+0800  任務1開始執行
2019-04-20 23:27:54.082177+0800  任務2開始執行
2019-04-20 23:27:54.082255+0800 任務3開始執行
2019-04-20 23:27:55.087043+0800 任務1執行完了
2019-04-20 23:27:56.087664+0800 任務2執行完了
2019-04-20 23:27:57.083123+0800  任務3執行完了

??我們往全局并發隊列里異步添加了三個任務,三個任務幾乎同時開始執行。這里需要注意的是并不是向隊列里添加了多少個任務,這些任務就會全部一起執行,這得看CPU的運行狀況,系統會選擇幾個任務一起執行,剩下的任務等先執行的任務執行完了再執行。

5. 向主隊列中添加任務

??主隊列的任務默認都是在主線程中執行的,所以如果我們有需要更新UI或者處理用戶交互的任務就必須放在主隊列中。這里需要注意的是只能異步向主隊列中添加任務,否則會發生死鎖。

- (void)viewDidLoad {
    [super viewDidLoad];
 
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"task 1");
    });
    
    NSLog(@"task 2");
}

日志

task1

??任務2并沒有執行,此時程序卡主了。這就叫做死鎖。這是因為任務2是直接添加在主線程里,任務1是添加在主隊列里的,主線程要執行完自己的任務,才會執行主隊列里的任務,所以任務1要等任務2執行完才能執行。而任務1又是同步添加的,它不執行完,下面的任務就不能執行。這就造成了任務1和任務2相互等待,誰都不執行。
任務1對任務2說:你快執行???你執行完我才能執行!
任務2對任務1說:不行啊,你執行完我們才能執行。這是同步那個二貨規定的。
任務1和任務2就開始了漫長的等待……

6.dispatch_group_t的使用

??有這樣一個需求,一張圖片需要兩張圖片合成,而這兩張圖片是分別下載的,所以我們要等兩張圖片都下載完了,才能合成。由于我們不知道哪張圖片先下載完,只能是一張下載完了再下載另一張,最后合成。這樣浪費時間,因為我們是可以同時兩張圖片的。
??GCD提供了隊列組,只有隊列里的任務全部執行完,才會執行回調。這樣我們就可以同時下載兩張圖片了,而不用擔心合成的時候有一張沒下載完。

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView1;
@property (weak, nonatomic) IBOutlet UIImageView *imageView2;
@property (weak, nonatomic) IBOutlet UIImageView *imageView3;
@end

@implementation ViewController
// 點擊屏幕開始下載圖片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    // 獲得全局并發隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 創建隊列組
    dispatch_group_t group = dispatch_group_create();
    
    __block UIImage *image1 = nil;
    // 開啟一個任務
    dispatch_group_async(group, queue, ^{
        NSLog(@"%@開始下載第一張圖片",[NSThread currentThread]);
        NSString *strURL1 = @"http://h.hiphotos.baidu.com/zhidao/pic/item/6d81800a19d8bc3ed69473cb848ba61ea8d34516.jpg";
        image1 = [self downloadImageWithURL:strURL1];
    });
    
    // 開啟一個任務
    __block UIImage *image2 = nil;
    dispatch_group_async(group, queue, ^{
        NSLog(@"%@開始下載第二張圖片",[NSThread currentThread]);
        NSString *strURL2 = @"http://h.hiphotos.baidu.com/zhidao/pic/item/0eb30f2442a7d9334f268ca9a84bd11372f00159.jpg";
        image2 = [self downloadImageWithURL:strURL2];
    });
    
    // 同時執行下載圖片1\下載圖片2的操作
    
    // 等group里的任務全部執行完畢,執行 dispatch_group_notify回調
    // 回到主線程顯示圖片
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       NSLog(@"%@顯示圖片",[NSThread currentThread]);
        self.imageView1.image = image1;
        self.imageView2.image = image2;
        // 合并兩張圖片圖片
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 50), NO, 0.0);
        [image1 drawInRect:CGRectMake(0, 0, 50, 50)];
        [image2 drawInRect:CGRectMake(50, 0, 50, 50)];
        self.imageView3.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    });
}
- (UIImage *)downloadImageWithURL : (NSString *)strURL {
    NSURL *url = [NSURL URLWithString:strURL];
    return [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
}
@end

日志

2016-11-05 23:03:49.300 <NSThread: 0x7ba80500>{number = 3, name = (null)}開始下載第一張圖片
2016-11-05 23:03:49.301 <NSThread: 0x7ba80b10>{number = 4, name = (null)}開始下載第二張圖片
2016-11-05 23:03:49.453 <NSThread: 0x79773680>{number = 1, name = main}顯示圖片

效果:

屏幕快照 2016-11-05 上午9.08.38.png

7.延遲執行

??延遲執行就是讓程序過一會再執行某個任務。有兩種方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 第一種
    [self performSelector:@selector(run) withObject:nil afterDelay:2];
    // 第二種 可以安排執行的隊列 3秒后執行
    // 主隊列
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"當前線程%@",[NSThread currentThread]);
        NSLog(@"GCD主隊列過一會執行我");
    });
    // 全局并發隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
        NSLog(@"當前線程3%@",[NSThread currentThread]);
        NSLog(@"GCD全局并發隊列過一會執行我");
    });
}
- (void)run {
    NSLog(@"當前線程1%@",[NSThread currentThread]);
    NSLog(@"過一會執行我");
}

日志

當前線程1<NSThread: 0x7b6629a0>{number = 1, name = main}
過一會執行我
當前線程2<NSThread: 0x7b6629a0>{number = 1, name = main}
當前線程3<NSThread: 0x7d175a90>{number = 3, name = (null)}
GCD主隊列過一會執行我
 GCD全局并發隊列過一會執行我

??延遲執行我們常用的可能是第一種方法。從那個線程中調用“ performSelector”,run方法就在哪個線程中執行可,一般是主線程。第二種方法是可以自定義方法執行的隊列,可以是主隊列,也可以是全局隊列。本人比較喜歡用block,所以喜歡第二種,因為都寫在一起,增加了代碼的可讀性。

8.一次性代碼

??一次性代碼主要是在單例中應用。

#import "Person.h"

@implementation Person

static Person *person;
// 1
- (instancetype)shareInstance1 {
    
    static dispatch_once_t onceToken;
    // block里的代碼只在一個進程周期中執行一次
    dispatch_once(&onceToken, ^{
        person = [[Person alloc] init];
    });
    return person;
}
// 2.
- (instancetype)shareInstace2 {
    
    if (!person) {
        person = [[Person alloc] init];
    }
    return person;
}
@end

??dispatch_once里的代碼在整個程序運行過程中就執行一次?。?!所以你有這方面的需求,也可以用這個。

??關于GCD還有很多強大的函數,并且是開源的,歡迎大家補充說明,不吝賜教。

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

推薦閱讀更多精彩內容