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

从上面的代码可以看到,我们是在系统的“全局并发队列”里进行的请求,最后在“主队列“里进行的更新。这样我们对队列的影响性和需求性相比栅栏函数就降低了。
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

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

你可能感兴趣的:(GCD 学习总结)