iOS: GCD详尽总结

字数 2120阅读 52

GCD编程的核心就是dispatch队列,dispatch block的执行最终都会放进某个队列中去进行。gcd中相关函数的使用一般都是以dispatch开头

一、函数和队列

1、GCD 中的两个常用函数
1、异步函数(async): 可以在新的线程中执行任务, 具备开启新线程的能力;
2、同步函数(sync): 只能在当前线程中执行任务, 不具备开新启线程的能力;

queue: 队列  bolck: 任务
异步函数
dispatch_async(dispatch_queue_t queue, ^{
// block 内容
});
同步函数
dispatch_sync(dispatch_queue_t queue, ^{
// block 内容
});

2、GCD的队列类型
1、并发队列(Concurrent Dispatch Queue)
2、串行队列(Serial Dispatch Queue)

a.并发队列:可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步(dispatch_async)函数下才有效。

(并发的意思是可以同时执行第一个任务 但是是时间是分片的,每刻只有一个任务执行 就是执行这个任务一会 再执行另外任务 交替进行的)

创建并发队列
dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT);
全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

b.串行队列:让任务一个接着一个的执行 (一个任务执行完毕再执行下一个任务)

创建串行队列
dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_SERIAL);

获取主队列
//dispatch_get_main_queue()获得主队列
主队列是一种特殊串行队列;

和其它串行队列一样,这个队列中的任务一次只能执行一个。
然而,它能保证所有的任务都在主线程执行,主线程是唯一可用于更新 UI 的线程。

二、GCD 的基本使用

1. 异步函数+并发队列:开启多条线程,并发执行任务
2. 异步函数+串行队列:开启一条线程,串行执行任务
3. 异步函数+主队列:不开线程,在主线程中串行执行任务
4. 同步函数+并发队列:不开线程,串行执行任务
5. 同步函数+串行队列:不开线程,串行执行任务
6. 同步函数+主队列:不开线程,串行执行任务(注意死锁发生)

1.异步函数和并发队列

dispatch_queue_t queue = dispatch_queue_create("asyncConcurrent", DISPATCH_QUEUE_CONCURRENT);
// 也可以获取全局并发队列,执行效果是一样的
// dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{ NSLog(@"1 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"2 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"3 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"4 %@", [NSThread currentThread]); });

结果:
4 <NSThread: 0x60000026fd00>{number = 6, name = (null)}
2 <NSThread: 0x600000270e40>{number = 4, name = (null)}
1 <NSThread: 0x600000270f00>{number = 3, name = (null)}
3 <NSThread: 0x60000026c400>{number = 5, name = (null)}

可以看出队列开启了4条子线程区分别执行4个任务, 队列中的任务是并发执行的. 
但是在这里有个注意点: 并不是说有多少任务, GCD就会开启多少条线程,
具体开启几条线程是不确定的, 是由系统决定的.

2.异步函数和串行队列

dispatch_queue_t queue = dispatch_queue_create("asyncSerial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ NSLog(@"1 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"2 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"3 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"4 %@", [NSThread currentThread]); });

结果:
1 <NSThread: 0x60000007e400>{number = 3, name = (null)}
2 <NSThread: 0x60000007e400>{number = 3, name = (null)}
3 <NSThread: 0x60000007e400>{number = 3, name = (null)}
4 <NSThread: 0x60000007e400>{number = 3, name = (null)}

队列只开启了一条子线程, 去一个接着一个任务去执行. 这种方式对任务的执行效率没有任何提高.

3.异步函数和主队列

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{ NSLog(@"1 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"2 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"3 %@", [NSThread currentThread]); });
dispatch_async(queue, ^{ NSLog(@"4 %@", [NSThread currentThread]); });

结果:
1 <NSThread: 0x60000007a600>{number = 1, name = main}
2 <NSThread: 0x60000007a600>{number = 1, name = main}
3 <NSThread: 0x60000007a600>{number = 1, name = main}
4 <NSThread: 0x60000007a600>{number = 1, name = main}

主队列所有的任务确实是在主线程执行的, 虽然是异步函数, 但也不会开启线程.

4.同步函数和并发队列

//第一个参数: C语言的字符串,标签
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{ NSLog(@"test1- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"test2- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"test3- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"test4- %@", [NSThread currentThread]); });

打印结果:
test1- <NSThread: 0x608000078dc0>{number = 1, name = main}
test2- <NSThread: 0x608000078dc0>{number = 1, name = main}
test3- <NSThread: 0x608000078dc0>{number = 1, name = main}
test4- <NSThread: 0x608000078dc0>{number = 1, name = main}

同步函数是不会开启子线程的, 所有任务都是在主线程中串行执行的.

5.同步函数和串行队列

dispatch_queue_t queue = dispatch_queue_create("syncSerial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{ NSLog(@"syncSerial-1- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"syncSerial-2- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"syncSerial-3- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"syncSerial-4- %@", [NSThread currentThread]); });

打印结果:
syncSerial-1- <NSThread: 0x60800007b100>{number = 1, name = main}
syncSerial-2- <NSThread: 0x60800007b100>{number = 1, name = main}
syncSerial-3- <NSThread: 0x60800007b100>{number = 1, name = main}
syncSerial-4- <NSThread: 0x60800007b100>{number = 1, name = main}

同样, 此时也是不会创建子线程的, 所有任务是在主线程中也是串行执行, 和4的情况是一样的效果.

6.同步函数和主队列

NSLog(@"---begin---"); 
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{ NSLog(@"syncMain1- %@", [NSThread currentThread]); }); 
dispatch_sync(queue, ^{ NSLog(@"syncMain2- %@", [NSThread currentThread]); });
dispatch_sync(queue, ^{ NSLog(@"syncMain3- %@", [NSThread currentThread]); }); 
NSLog(@"---end---");

结果(只有这一行输出,死锁的情况):
---begin---

由队列引起的循环等待。

三、GCD 常用函数

1. dispatch_barrier_async函数(栅栏函数)

它等待所有位于barrier函数之前的操作执行完毕后执行, 并且在barrier函数执行之后, barrier函数之后的操作才会得到执行, 该函数需要同dispatch_queue_create函数生成的并发队列(concurrent Dispatch Queue)一起使用

dispatch_barrier_async代码实例:

//同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用
dispatch_queue_t queue = dispatch_queue_create("123", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{NSLog(@"--1--%@", [NSThread currentThread]);});
dispatch_async(queue, ^{NSLog(@"--2--%@", [NSThread currentThread]);});
dispatch_barrier_async(queue, ^{NSLog(@"--barrier--%@", [NSThread currentThread]);});
dispatch_async(queue, ^{NSLog(@"--3--%@", [NSThread currentThread]);});
dispatch_async(queue, ^{NSLog(@"--4--%@", [NSThread currentThread]);});

输出结果:1 2 --> barrier -->3 4  其中12 与 34 由于并行处理先后顺序不定

下面是你何时会,和不会,使用障碍函数的情况:

自定义串行队列:一个很坏的选择;障碍不会有任何帮助,因为不管怎样,一个串行队列一次都只执行一个操作。

全局并发队列: 要小心,这可能不是最好的主意,因为其它系统可能在使用队列而且你不能垄断它们只为你自己的目的。

自定义并发队列:这对于原子或临界区代码来说是极佳的选择。任何你在设置或实例化的需要线程安全的事物都是使用障碍的最佳候选。

2. dispatch_after函数延迟处理任务
在实际的开发中,经常会遇到想要在指定的时间间隔后执行某个处理

2.1 默认在主线程中执行的

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 
       NSLog(@"GCD-%@", [NSThread currentThread]); 
});
结果: GCD-<NSThread: 0x60000006b4c0>{number = 1, name = main}

2.2 可以修改任务任务执行所在的线程. 任务是在子线程中执行的.

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), queue, ^{
      NSLog(@"GCD-%@", [NSThread currentThread]); 
});
结果: GCD-<NSThread: 0x60000006a3c0>{number = 3, name = (null)}

何时适合使用 dispatch_after 比较合适?
自定义串行队列: 在一个自定义串行队列上使用 dispatch_after 要小心。你最好坚持使用主队列。
主队列(串行): 是使用 dispatch_after 的好选择;Xcode 提供了一个不错的自动完成模版。
并发队列: 在并发队列上使用 dispatch_after 也要小心;你会这样做就比较罕见。还是在主队列做这些操作吧。

补充
NSObject中提供的线程延迟方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
通过NSTimer来延迟线程执行
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:NO];

3. once 一次性执行

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
      NSLog(@"once - %@", [NSThread currentThread]);
});
它在整个运行程序中只会执行一次, 
GCD 的一次性执行代码一般都是用在单例设计模式中.保证全局只有一个对象实例.

4. Group queue (队列组):
将多线程进行分组,最大的好处是可获知所有线程的完成情况。Group queue 可以通过调用dispatch_group_create()来获取,通过dispatch_group_notify, 可以直接监听组里所有线程完成情况。

// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 创建队列组
dispatch_group_t group = dispatch_group_create();
//队列组异步执行任务
dispatch_group_async(group, queue, ^{ NSLog(@"任务1--%@", [NSThread currentThread]); });
dispatch_group_async(group, queue, ^{ NSLog(@"任务2--%@", [NSThread currentThread]); });
dispatch_group_async(group, queue, ^{ NSLog(@"任务3--%@", [NSThread currentThread]); });
// 队列组拦截通知模块(内部本身是异步执行的,不会阻塞线程)
dispatch_group_notify(group, queue, ^{ NSLog(@"--队列组任务执行完毕--"); });

打印结果:
任务2--<NSThread: 0x60800007e140>{number = 8, name = (null)}
任务1--<NSThread: 0x60800007e040>{number = 6, name = (null)}
任务3--<NSThread: 0x60800007f6c0>{number = 9, name = (null)}
--队列租任务执行完毕--
任务1--<NSThread: 0x60800007f6c0>{number = 9, name = (null)}
任务2--<NSThread: 0x60800007e040>{number = 6, name = (null)}
任务3--<NSThread: 0x60800007e140>{number = 8, name = (null)}
--队列租任务执行完毕--
任务1--<NSThread: 0x60800007e040>{number = 6, name = (null)}
任务2--<NSThread: 0x60800007e140>{number = 8, name = (null)}
任务3--<NSThread: 0x60800007f6c0>{number = 9, name = (null)}
--队列租任务执行完毕--

4.1 dispatch_group_enter / leave
和内存管理的引用计数类似,我们可以认为group也持有一个整形变量(只是假设),当调用enter时计数加1,调用leave时计数减1,当计数为0时会调用dispatch_group_notify并且dispatch_group_wait会停止等待;

NSLog(@"1");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_notify(group, queue, ^{
        NSLog(@"2");
});
    
dispatch_group_enter(group);
dispatch_sync(queue, ^{
        NSLog(@"3");
});
dispatch_group_leave(group);

输出132
-------------------------------------------------
dispatch_group_t group =dispatch_group_create();
dispatch_queue_t globalQueue=dispatch_get_global_queue(0, 0);

dispatch_group_enter(group);
//模拟多线程耗时操作
dispatch_async(globalQueue, ^{
    sleep(5);
    NSLog(@"block1完成");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
//模拟多线程耗时操作
dispatch_async(globalQueue, ^{
    sleep(3);
    NSLog(@"block2完成");
    dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"全部结束");
});

block2完成
block1完成
全部结束

block里执行的是同步类型的代码那么用dispatch_group_async一样可以达到同步的效果,但是异步任务就不行了如下:

dispatch_queue_t dispatchQueue = dispatch_queue_create("ted.queue.next1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    dispatch_async(globalQueue, ^{
        sleep(3);
        NSLog(@"任务一完成");
    });
});

dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    dispatch_async(globalQueue, ^{
        sleep(5);
        NSLog(@"任务二完成");
    });
});

dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
    NSLog(@"notify:任务都完成了");
});

如果dispatch_group_async里执行的是异步代码dispatch_group_notify会直接触发而不会等待异步任务完成,而dispatch_group_enter、和dispatch_group_leave则不会有这个问题,它们只需要在任务开始前enter结束后leave即可达到线程同步的效果。

dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
        dispatch_group_enter(dispatchGroup);
        dispatch_async(globalQueue, ^{
            sleep(5);
            NSLog(@"任务二完成");
            dispatch_group_leave(dispatchGroup);
        });
});

4.2 dispatch_group_wait
和dispatch_group_notify功能类似(多了一个dispatch_time_t参数可以设置超时时间),在group上任务完成前,dispatch_group_wait会阻塞当前线程(所以不能放在主线程调用)一直等待;当group上任务完成,或者等待时间超过设置的超时时间会结束等待;

5. GCD定时器
GCD定时器的优势
不受RunLoop的运行模式的影响(因为它的底层也是C语言)
大家在开发的过程中,经常会用到定时器,通常的做法可能就是NSTimer,了解过GCD的同学可能会接触到dispatch source的概念,dispatch source是一个监视某些类型事件的对象。

1.dispatch_source_create
创建一个新的调度源来监视低级别的系统对象和自动提交处理程序块来响应事件调度队列

2.dispatch_source_set_timer
为一个定时源设置一个开始时间、事件间隔、误差值
我们来看看这个函数原型
dispatch_source_set_timer(dispatch_source_t source,
                          dispatch_time_t start,
                          uint64_t interval,
                          uint64_t leeway);
source当然就是我们第一步创建的调度源。
start是我们设定的计时开始时间,可以dispatch_time 和 dispatch_walltime 函数来创建它们.

3.dispatch_source_set_event_handler
给一个调度源设置一个时间处理块。

4.dispatch_source_cancel
异步取消一个调度源,防止任何进一步调用它的事件处理块的发生

5.dispatch_source_set_cancel_handler
给一个调度源设置一个取消处理块

6.dispatch_resume
同步等待一个对象,直到超时

以上几个步骤中,dispatch_source_set_event_handler与dispatch_source_cancel必须一起出现,
否则调度源处理块不会执行。

代码示例:
//dispatch_walltime(NULL, 0)可以换成DISPATCH_TIME_NOW
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,queue);
dispatch_source_set_timer(_timer,
                          dispatch_walltime(NULL, 0),
                          1.0*NSEC_PER_SEC,
                          0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
     //在这里执行事件
     dispatch_source_cancel(_timer);
});
//开始执行dispatch源
dispatch_resume(_timer);

6、快速迭代
使用dispatch_apply函数能进行快速迭代遍历

dispatch_async(dispatch_get_global_queue(0, 0), ^(){
        dispatch_apply(10 , dispatch_get_global_queue(0, 0), ^(size_t index){
            // 执行10次代码,index顺序不确定
            NSLog(@"%ld", index);
        });
        NSLog(@"done");
});

输出 index 顺序不确定,因为它是并行执行的(dispatch_get_global_queue是并行队列),但是done是在以上操作完成后才会执行,因此,它一般都是放在dispatch_async里面(异步)。

实际上,这里 dispatch_apply如果换成串行队列上,则会依次输出index,但这样违背了我们想并行提高执行效率的初衷。

7、dispatch_semaphore
信号量基于计数器的一种多线程同步机制。在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。

3个相关方法:

  1. dispatch_semaphore_t dispatch_semaphore_create(long value)方法接收一个long类型的参数, 返回一个dispatch_semaphore_t类型的信号量,值为传入的参数
  2. dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)接收一个信号和时间值,若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者经过输入的时间值;若信号量大于0,则会使信号量减1并返回,程序继续住下执行
  3. dispatch_semaphore_signal(dispatch_semaphore_t dsema)使信号量加1并返回

7.1:保持线程同步

dispatch_queue_t queue = dispatch_get_global_queue(0, 0); 
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 
__block int j = 0; 
dispatch_async(queue, ^{ 
      j = 100; 
      dispatch_semaphore_signal(semaphore); 
}); 
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 
NSLog(@"finish j = %zd", j);

结果输出 j = 100;
如果注掉dispatch_semaphore_wait这一行,则 j = 0;

由于是将block异步添加到一个并行队列里面,所以程序在主线程跃过block直接到dispatch_semaphore_wait这一行,因为semaphore信号量为0,时间值为DISPATCH_TIME_FOREVER,所以当前线程会一直阻塞,直到block在子线程执行到dispatch_semaphore_signal,使信号量+1,此时semaphore信号量为1了,所以程序继续往下执行。这就保证了线程间同步了。

7.2:为线程加锁

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); 
for (int i = 0; i < 100; i++) { 
      dispatch_async(queue, ^{ 
            // 相当于加锁 
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 
            NSLog(@"i = %zd semaphore = %@", i, semaphore); 
            // 相当于解锁 
            dispatch_semaphore_signal(semaphore); 
      }); 
}

注释:当线程1执行到dispatch_semaphore_wait这一行时,semaphore的信号量为1,所以使信号量-1变为0,并且线程1继续往下执行;如果当在线程1NSLog这一行代码还没执行完的时候,又有线程2来访问,执行dispatch_semaphore_wait时由于此时信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会一直阻塞线程2(此时线程2处于等待状态),直到线程1执行完NSLog并执行完dispatch_semaphore_signal使信号量为1后,线程2才能解除阻塞继续住下执行。以上可以保证同时只有一个线程执行NSLog这一行代码。

附:
GCD使用三部曲之:基本用法里面提到“挂起和恢复队列‘’

推荐阅读更多精彩内容