GCD 学习总结

一、GCD 简介:

全称是 Grand Central Dispatch,是纯 C 语言,提供了非常多强大的函数。

GCD 的优势:

GCD 是苹果公司为多核的并行运算提出的解决方案
GCD 会自动利用更多的 CPU 内核(比如双核、四核)
GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

二、GCD 最根本的三步骤:

任务 添加到 队列,并且指定执行任务的 函数
下面是最基础的写法:

把任务添加到队列 --> 函数
// 1、任务
dispatch_block_t block = ^{
    NSLog(@"hello GCD");
}

// 2、串行队列
dispatch_queue_t queue = dispatch_queue_create("com.fanjiduo.cn", NULL);

// 3、异步函数指定要执行的队列,并指定当前要执行的block
dispatch_async(queue, block);
  • 任务使用 block 封装
  • 任务的 block 没有参数也没有返回值

GCD 有三个关键的名词,分别是函数、队列和任务

1、函数

执行任务的函数有两种

1)、异步 dispatch_async

  • 不用等待当前语句执行完毕,就可以执行下一条语句
  • 会开启线程执行 block 的任务
  • 异步是多线程的代名词

2)、同步 dispatch_sync

  • 必须等待当前语句执行完毕,才会执行下一条语句
  • 不会开启线程
  • 在当前线程执行 block 的任务
2、队列

队列是一种数据结构,支持FIFO(先进先出)
先进去,先调度。
串行队列
并发队列


系统默认提供了两个队列,主队列和 global 全局队列。
主队列是串行队列
dispatch_get_main_queue();
global 是并发队列
dispatch_get_global_queue(0,0);
不建议直接使用全局并发队列 global,因为这是系统提供了,很多地方都会用,最好剥离开来,自己创建。

面试题:下面三段代码打印顺序分别是什么?
代码1
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");
答案:代码 1 因为是并发队列,并且都是异步 async 请求,异步不阻塞,但是开辟线程需要耗时,所以 1 之后是 5,2 之后是 4,所以打印顺序为 1、5、2、4、3。
代码2
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");
答案:代码 2 因为是并发队列,异步 async 开辟线程需要耗时,而同步sync不开辟线程,会阻塞,所以打印顺序为1、5、2、3、4。
代码3
dispatch_queue_t queue = dispatch_queue_create("fanjiduo", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{//block1
    NSLog(@"2");
    dispatch_sync(queue, ^{//block2
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");
答案:打印1 5 2 之后在下面图中位置闪退:

这是因为造成了 死锁
为什么会造成死锁,看下图这个队列:

block2 加入队列以后,会把打印任务 4 加入到队列,然后再把打印任务 3 加入到队列,但是由于是串行+同步,block2 要等待 3 执行完毕才能执行 4,因为遵循 FIFO(先进先出原则),所以 3 又要等待 4 执行完才能执行,互相等待,所以就造成了 死锁(DeadLock)

关于死锁再看看下面的代码:

dispatch_sync(dispatch_get_main_queue(), ^(void){
    NSLog(@"这里死锁了");
});

执行这个主队列的是主线程,执行了同步函数后,将 block 添加到了主队列中,同时调用 dispatch_syn 这个函数的线程被阻塞(也就是主线程被阻塞),等待 block 执行完成,而执行主线程队列任务的线程正是主线程,此时他处于阻塞状态,所以 block 永远不会被执行,因此主线程一直处于阻塞状态,并非卡在 block 中无法返回,而是根本无法执行到这个 block

面试题2:
int a = 0;
while (a < 10) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
    })
 }
NSLog(@"%d", a);

1问:a 为什么会报错?
答:要加 __block
2问:a 输出的值是什么?
答:有多种可能,但肯定是大于等于 10 的,因为 a=10 出循环的时候,可能还有其他线程没有执行完毕,所以最终 a 可能大于 10

三、GCD 的应用

1、栅栏函数 dispatch_barrier_async

栅栏函数最直接的作用:控制任务执行顺序。

栅栏函数一定要是自定义的并发队列,不能用系统提供的队列。不然没有效果。(系统设计的就是这样,不然阻塞了系统提供的全局并发队列,那就GG了)

dispatch_barrier_sync(concurrentQueue, ^{
    NSLog(@"-------栅栏阻塞----------",[NSThred currentThread]);
});

问:下面的代码有什么问题?为什么?该如何解决?

//dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
dispatch_queue_t concurrentQueue = dispatch_queue_create("fanjiduo", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 2000; i++) {
    dispatch_async(concurrentQueue, ^{
        NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
        NSURL *url = [NSBundle mainBundel] URLForResource:imageName withExtension:nil];
        NSData *data = [NSData dataWithContentsOfURL:data];
        UIImage *image = [UIImage iamgeWithData:data];
        [self.mArray addObject:image];
    });
}

答:
问题1:
应该把系统的全局并发队列改为:

dispatch_queue_t concurrentQueue = dispatch_queue_create("fanjiduo", DISPATCH_QUEUE_CONCURRENT);

问题2:
addObject 的地方会崩溃,报的错误是 Thread 7:signal SIGABRT,是线程 BUG
会产生资源抢夺。可以给 addObject 加锁

@synchronized (self) {
    [self.mArray addObject:image]; 
}

或者加入一个栅栏函数:

dispatch_barrier_async(concurrentQueue, ^{
    [self.mArray addObject:image];
});

栅栏函数也可以保证线程安全。

dispatch_barrier_asyncdispatch_barrier_sync 的区别:
dispatch_barrier_async(concurrentQueue, ^{
});

是只堵塞 concurrentQueue 里的东西,不堵塞其他线程。而
dispatch_barrier_sync(concurrentQueue, ^{
});

是堵塞这行代码之后的所有东西,不管是什么线程。

栅栏函数注意点:

  • 一定要是自定义的并发队列
  • 必须要求都在同一个队列(缺点:不利于封装,太依赖于必须同一个队列了)

2、调度组

如何避免栅栏函数的缺点,又能实现功能呢?
我们可以使用调度组。

关键函数:
dispatch_group_create 创建组
dispatch_group_async 进组任务
dispatch_group_notify 进组任务执行完毕通知
dispatch_group_wait 进组任务执行等待时间
dispatch_group_enter 进组
dispatch_group_leave 出组

下面是用调度组实现类似上面栅栏函数功能的代码:

//创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue1 = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);

// SIGNAL
dispatch_group_async(group, queue, ^{
    NSString *logoStr = @"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3351002169,4211425181&fm=27&gp=0.jpg";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});
    
dispatch_group_async(group, queue1, ^{
    NSString *logoStr = @"https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3033952616,135704646&fm=27&gp=0.jpg";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});

__block UIImage *newImage = nil;
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"数组个数:%ld",self.mArray.count);
    for (int i = 0; i<self.mArray.count; i++) {
        UIImage *waterImage = self.mArray[i];
        newImage = [KC_ImageTool kc_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 100, 40)];
    }
    self.imageView.image = newImage;
});

从上面的代码可以看到,我们是在系统的“全局并发队列”里进行的请求,最后在“主队列“里进行的更新。这样我们对队列的影响性和需求性相比栅栏函数就降低了。
dispatch_group_t group = dispatch_group_create();

long timeout = dispatch_group_wait(group, 2);

除了上面的写法,还有一种进组和出组的写法:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);

// dispatch_group_async -- 下节课的源码分析 --封装意思
dispatch_async(queue, ^{
    NSLog(@"第一个走完了");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"第二个走完了");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务完成,可以更新UI");
});

dispatch_group_enterdispatch_group_leave 必须成对出现。

底层原理是:
dispatch_group_enter(group) 进组以后,组就会处于一个休眠状态,内部有一个 signal 信号就会 +1dispatch_group_leave 出组 signal 就会 -1,然后不停的循环判断 signal 是否等于 0,如果 signal == 0 了,那么就会调用 group_wakeup 函数,组就会复苏,就会隐性的调用 dispatch_group_notify 通知。

signal 不能为小于 0 的数,否则会崩溃。

dispatch_group_asyncdispatch_async 的区别是什么?(看libdispatch-913.60.2源码)

3、信号量 dispatch_semaphore_t

GCD 中还有个东西叫做信号量 ** dispatch_semaphore_t**,它可以控制并发数,也可以当成锁来使用。

关键函数:
dispatch_semaphore_create 创建信号量
dispatch_semaphore_wait 信号量等待
dispatch_semaphore_signal 信号量释放

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 信号量 -- gcd控制并发数
    // 同步
    //总结:由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    
    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务1");
        sleep(1);
        NSLog(@"任务1完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务2");
        sleep(1);
        NSLog(@"任务2完成");
        dispatch_semaphore_signal(semaphore);
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务3");
        sleep(1);
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(semaphore);
    });

打印结果为:

执行任务2
执行任务1
任务2完成
任务1完成
执行任务3
任务3完成

通过上面的代码可以看出,dispatch_semaphore_create(2),信号量设为的是 2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过 2。所以会等到任务 12 都执行完才会执行任务 3

转载请备注原文出处,不得用于商业传播——凡几多

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269