本篇将从四个方面对iOS开发中GCD的使用进行详尽的讲解:程序员
1、什么是GCD
2、咱们为何要用GCD技术
3、在实际开发中如何使用GCD更好的实现咱们的需求
1、Synchronous & Asynchronous 同步 & 异步
2、Serial Queues & Concurrent Queues 串行 & 并发
3、Global Queues全局队列
4、Main Queue主队列
5、同步的做用
6、dispatch_time延迟操做
7、线程安全(单例dispatch_once、读写dispatch_barrier_async)
8、调度组(dispatch_group)
4、定时源事件和子线程的运行循环
1、什么是GCD
GCD 是基于 C 的 API,它是 libdispatch
的市场名称,而 libdispatch 做为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。数组
2、咱们为何要用GCD技术
- GCD 能经过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
- GCD 提供一个易于使用的并发模型而不只仅只是锁和线程,以帮助咱们避开并发陷阱。
- GCD 具备在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
- GCD旨在替换NSThread等线程技术
- GCD可充分利用设备的多核
- GCD可自动管理线程的生命周期
3、在实际开发中如何使用GCD更好的实现咱们的需求
1、Synchronous & Asynchronous 同步 & 异步
1)同步任务执行方式:在当前线程中执行,必须等待当前语句执行完毕,才会执行下一条语句安全

#pragma mark #pragma mark - 同步方法 /** 同步的打印顺序 打印 begin 打印 [NSThread currentThread] 打印 end */ - (void)syncTask { NSLog(@"begin"); // 1.GCD同步方法 /** 参数1:队列 第一个参数0其实为队列优先级DISPATCH_QUEUE_PRIORITY_DEFAULT,若是要适配 iOS 7.0 & 8.0,则始终为0 参数2:任务 */ dispatch_sync(dispatch_get_global_queue(0, 0), ^{ // 任务中要执行的代码 NSLog(@"%@", [NSThread currentThread]); }); NSLog(@"end"); }
2)异步任务执行方式:不在当前线程中执行,不用等待当前语句执行完毕,就能够执行下一条语句网络

#pragma mark #pragma mark - 异步方法 /** 异步的打印顺序 打印 begin 打印 通常状况下为end,极少数状况下会很快开辟完新的线程,先打印出[NSThread currentThread] */ - (void)asyncTask { /** 异步:不会在“当前线程”执行,会首先去开辟新的子线程,开辟线程须要花费时间 */ NSLog(@"begin"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%@", [NSThread currentThread]); }); NSLog(@"end"); }
2、Serial Queues & Concurrent Queues 串行 & 并发
1)串行队列调度同步和异步任务执行多线程
串行队列特色:
以先进先出的方式,顺序调度队列中的任务执行
不管队列中所指定的执行任务函数是同步仍是异步,都会等待前一个任务执行完成后,再调度后面的任务并发

#pragma mark #pragma mark - 串行队列同步方法 /** 串行队列,同步方法 1.打印顺序 : 从上到下,依次打印,由于是串行的 2.在哪条线程上执行 : 主线程,由于是同步方法,因此在当前线程里面执行,刚好当前线程是主线程,因此它就在主线程上面执行 应用场景:开发中不多用 */ - (void)serialSync { // 1.建立一个串行队列 /** 参数1:队列的表示符号,通常是公司的域名倒写 参数2:队列的类型 DISPATCH_QUEUE_SERIAL 串行队列 DISPATCH_QUEUE_CONCURRENT 并发队列 */ dispatch_queue_t serialQuene = dispatch_queue_create("com.baidu", DISPATCH_QUEUE_SERIAL); // 建立任务 void (^task1) () = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2) () = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3) () = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; // 添加任务到队列,同步方法执行 dispatch_sync(serialQuene, task1); dispatch_sync(serialQuene, task2); dispatch_sync(serialQuene, task3); }

#pragma mark #pragma mark - 串行队列异步方法 /** 串行队列,异步方法 1.打印顺序:从上到下,依次执行,它是串行队列 2.在哪条线程上执行:在子线程,由于它是异步执行,异步就是不在当前线程里面执行 应用场景:耗时间,有顺序的任务 1.登陆--->2.付费--->3.才能看 */ - (void)serialAsync { // 除了第三步,和串行同步方法中都是同样的 // 1.建立一个串行队列 dispatch_queue_t serialQuene = dispatch_queue_create("com.baidu", DISPATCH_QUEUE_SERIAL); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; // 3.添加任务到队列 dispatch_async(serialQuene, task1); dispatch_async(serialQuene, task2); dispatch_async(serialQuene, task3); }
2)并发队列调度异步任务执行框架
并发队列特色:
以先进先出的方式,并发调度队列中的任务执行
若是当前调度的任务是同步执行的,会等待任务执行完成后,再调度后续的任务
若是当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,会再新的线程调度后续任务的执行异步

#pragma mark #pragma mark - 并发队列同步任务 /** 并发队列,同步任务 1.打印顺序:由于是同步,因此依次执行 2.在哪条线程上执行:主线程,由于它是同步方法,它就在当前线程里面执行,也就是在主线程里面依次执行 当并发队列遇到同步的时候仍是依次执行,因此说方法(同步/异步)的优先级会比队列的优先级高 * 只要是同步方法,都只会在当前线程里面执行,不会开子线程 应用场景: 开发中几乎不用 */ - (void)serialSync { /** 参数1:队列的表示符号,通常是公司的域名倒写 参数2:队列的类型 DISPATCH_QUEUE_SERIAL 串行队列 DISPATCH_QUEUE_CONCURRENT 并发队列 */ // 1.建立并发队列 dispatch_queue_t serialSync = dispatch_queue_create("com.xiaojukeji", DISPATCH_QUEUE_CONCURRENT); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; // 3.添加任务到并发队列 dispatch_sync(serialSync, task1); dispatch_sync(serialSync, task2); dispatch_sync(serialSync, task3); }

#pragma mark #pragma mark - 并发队列异步任务 /** 1.打印顺序:无序的 2.在哪条线程上执行:在子线程上执行,每个任务都在它本身的线程上执行 能够建立N条子线程,它是由底层可调度线程池来决定的,可调度线程池它是有一个重用机制 应用场景 同时下载多个影片 */ - (void)serialAsync { // 1.建立并发队列 dispatch_queue_t serialAsync = dispatch_queue_create("com.xiaojukeji", DISPATCH_QUEUE_CONCURRENT); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; // 3.将任务添加到并发队列 dispatch_async(serialAsync, task1); dispatch_async(serialAsync, task2); dispatch_async(serialAsync, task3); }
3、全局队列
全局队列是系统为了方便程序员开发提供的,其工做表现与并发队列一致async
全局队列 & 并发队列的区别ide
全局队列:没有名称,不管 MRC & ARC 都不须要考虑释放,平常开发中,建议使用"全局队列"
并发队列:有名字,和 NSThread 的 name 属性做用相似,若是在 MRC 开发时,须要使用 dispatch_release(q); 释放相应的对象
dispatch_barrier 必须使用自定义的并发队列
开发第三方框架时,建议使用并发队列
参数
参数1:服务质量(队列对任务调度的优先级)/iOS 7.0 以前,是优先级
iOS 8.0(新增,暂时不能用,今年年末)
QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(但愿最快完成-不能用太耗时的操做)
QOS_CLASS_USER_INITIATED 0x19, 用户指望(但愿快,也不能太耗时)
QOS_CLASS_DEFAULT 0x15, 默认(用来底层重置队列使用的,不是给程序员用的)
QOS_CLASS_UTILITY 0x11, 实用工具(专门用来处理耗时操做!)
QOS_CLASS_BACKGROUND 0x09, 后台
QOS_CLASS_UNSPECIFIED 0x00, 未指定,能够和iOS 7.0 适配
iOS 7.0
DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级
参数2:为将来保留使用的,应该永远传入0
结论:若是要适配 iOS 7.0 & 8.0,使用如下代码: dispatch_get_global_queue(0, 0);

#pragma mark #pragma mark - 全局队列同步任务 /** 全局队列,同步任务 1.打印顺序:依次执行,由于它是同步的 2.在哪条线程上执行:主线程,由于它是同步方法,它就在当前线程里面执行 当它遇到同步的时候,并发队列仍是依次执行,因此说,方法的优先级比队列的优先级高 * 只要是同步方法,都只会在当前线程里面执行,不会开子线程 应用场景:开发中几乎不用 */ - (void)globalSync { /** 参数1: IOS7:表示的优先级 IOS8:服务质量 为了保证兼容IOS7&IOS8通常传入0 参数2:将来使用,传入0 */ NSLog(@"begin"); // 1.建立全局队列 dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1----%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2----%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3----%@", [NSThread currentThread]); }; // 3.添加任务到全局队列 dispatch_sync(globalQueue, task1); dispatch_sync(globalQueue, task2); dispatch_sync(globalQueue, task3); NSLog(@"end"); }

#pragma mark #pragma mark - 全局队列异步任务 /** 全局队列,异步方法 1.打印顺序:无序的 2.在子线程上执行,每个任务都在它本身的线程上执行,线程数由底层可调度线程池来决定的,可调度线程池有一个重用机制 应用场景: 蜻蜓FM同时下载多个声音 */ - (void)globalAsync { NSLog(@"begin"); dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; dispatch_async(globalQueue, task1); dispatch_async(globalQueue, task2); dispatch_async(globalQueue, task3); NSLog(@"end"); }
4、主队列
特色
专门用来在主线程上调度任务的队列
不会开启线程
以先进先出的方式,在主线程空闲时才会调度队列中的任务在主线程执行
若是当前主线程正在有任务执行,那么不管主队列中当前被添加了什么任务,都不会被调度
队列获取
主队列是负责在主线程调度任务的
会随着程序启动一块儿建立
主队列只须要获取不用建立

#pragma mark #pragma mark - 主队列异步任务 /** 主队列,异步任务 1.执行顺序:依次执行,由于它在主线程里面执行 * 彷佛与咱们的异步任务有所冲突,可是由于它是主队列,因此,只在主线程里面执行 2.是否会开线程:不会,由于它在主线程里面执行 应用场景: 当作了耗时操做以后,咱们须要回到主线程更新UI的时候,就非它不可 */ - (void)mainAsync { NSLog(@"begin"); // 1.建立主队列 dispatch_queue_t mainAsync = dispatch_get_main_queue(); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; dispatch_async(mainAsync, task1); dispatch_async(mainAsync, task2); dispatch_async(mainAsync, task3); NSLog(@"end"); }

#pragma mark #pragma mark - 主队列同步方法有问题,不能用是个奇葩,会形成死锁 /** 主队列,同步任务有问题,不能用,彼此都在等对方是否执行完了,因此是互相死等 主队列只有在主线程空闲的时候,才会去调度它里面的任务去执行 */ - (void)mainSync { NSLog(@"begin"); // 1.建立主队列 dispatch_queue_t mainSync = dispatch_get_main_queue(); // 2.建立任务 void (^task1)() = ^() { NSLog(@"task1---%@", [NSThread currentThread]); }; void (^task2)() = ^() { NSLog(@"task2---%@", [NSThread currentThread]); }; void (^task3)() = ^() { NSLog(@"task3---%@", [NSThread currentThread]); }; // 3.添加任务到主队列中 dispatch_sync(mainSync, task1); dispatch_sync(mainSync, task2); dispatch_sync(mainSync, task3); NSLog(@"end"); }
Deadlock 死锁
两个(有时更多)东西——在大多数状况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操做。第一个不能完成是由于它在等待第二个的完成。但第二个也不能完成,由于它在等待第一个的完成。
5、同步的做用
同步任务,可让其余异步执行的任务,依赖
某一个同步任务,例如:在用户登陆以后,才容许异步下载文件!

#pragma mark #pragma mark - 模拟登陆下载多个电影数据 /** 同步的做用:保证咱们任务执行的前后顺序 1.登陆 2.同时下载三部电影 */ - (void)loadManyMovie { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%@", [NSThread currentThread]); // 1.登陆,同步在当前线程里面工做 dispatch_sync(dispatch_get_global_queue(0, 0), ^{ NSLog(@"登陆了---%@", [NSThread currentThread]); sleep(3); }); // 2.同时下载三部电影() dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下载第一个电影---%@", [NSThread currentThread]); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下载第二个电影---%@", [NSThread currentThread]); }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"正在下载第三个电影---%@", [NSThread currentThread]); }); dispatch_sync(dispatch_get_main_queue(), ^{ [NSThread sleepForTimeInterval:1.0]; NSLog(@"计算机将在三秒后关闭---%@", [NSThread currentThread]); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"关机了---%@", [NSThread currentThread]); }); }); }); }
6、dispatch_time延迟操做
不知道什么时候适合使用 dispatch_after
?
- 自定义串行队列:在一个自定义串行队列上使用
dispatch_after
要当心。你最好坚持使用主队列。 - 主队列(串行):是使用
dispatch_after
的好选择;Xcode 提供了一个不错的自动完成模版。 - 并发队列:在并发队列上使用
dispatch_after
也要当心;你会这样作就比较罕见。仍是在主队列作这些操做吧。

// MARK: - 延迟执行 - (void)delay { /** 从如今开始,通过多少纳秒,由"队列"调度异步执行 block 中的代码 参数 1. when 从如今开始,通过多少纳秒 2. queue 队列 3. block 异步执行的任务 */ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); void (^task)() = ^ { NSLog(@"%@", [NSThread currentThread]); }; // 主队列 // dispatch_after(when, dispatch_get_main_queue(), task); // 全局队列 // dispatch_after(when, dispatch_get_global_queue(0, 0), task); // 串行队列 dispatch_after(when, dispatch_queue_create("itheima", NULL), task); NSLog(@"come here"); } - (void)after { [self.view performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0]; NSLog(@"come here"); }
7、线程安全(单例dispatch_once、读写dispatch_barrier_async)
一个常见的担心是它们经常不是线程安全的。这个担心十分合理,基于它们的用途:单例经常被多个控制器同时访问。
单例的线程担心范围从初始化开始,到信息的读和写。
dispatch_once()
以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once
的代码)的不一样的线程会在临界区已有一个线程的状况下被阻塞,直到临界区完成为止。

// 使用 dispatch_once 实现单例 + (instancetype)sharedSingleton { static id instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }
线程安全实例不是处理单例时的惟一问题。若是单例属性表示一个可变对象,那么你就须要考虑是否那个对象自身线程安全。
若是问题中的这个对象是一个 Foundation 容器类,那么答案是——“极可能不安全”!Apple 维护一个有用且有些心寒的列表,众多的 Foundation 类都不是线程安全的。如:NSMutableArray。
虽然许多线程能够同时读取 NSMutableArray 的一个实例而不会产生问题,但当一个线程正在读取时让另一个线程修改数组就是不安全的。在目前的情况下不能预防这种状况的发生。GCD 经过用 dispatch barriers 建立一个读者写者锁,
提供了一个优雅的解决方案。
8、调度组(dispatch_group)

#pragma mark #pragma mark - 调度组 /** 调度组的实现原理:相似引用计数器进行+1和-1的操做 应用场景 好比同时开了三个线程下载视频,只有当三个视频彻底下载完毕后,我才能作后续的事 这个就须要用到调度组,这个调度组,就能监听它里面的任务是否都执行完毕 */ - (void)groupDispatch { // 1.建立调度组 dispatch_group_t group = dispatch_group_create(); // 2.获取全局队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // 3.建立三个下载任务 void (^task1) () = ^(){ NSLog(@"%@----下载片头",[NSThread currentThread]); }; dispatch_group_enter(group); // 引用计数+1 void (^task2) () = ^(){ NSLog(@"%@----下载中间的内容",[NSThread currentThread]); [NSThread sleepForTimeInterval:3.0]; NSLog(@"--下载中间内容完毕---"); dispatch_group_leave(group); // 引用计数-1 }; dispatch_group_enter(group); // 引用计数+1 void (^task3) () = ^(){ NSLog(@"%@----下载片尾",[NSThread currentThread]); dispatch_group_leave(group); // 引用计数-1 }; // 4.须要将咱们的队列 和 任务,加入到组内去监控 dispatch_group_async(group, queue, task1); dispatch_group_async(group, queue, task2); dispatch_group_async(group, queue, task3); // 5.监听的函数 /** 远离:来监听当调度组的引用计数器为0时,才会执行该函数中内容,不然不会执行 参数1:组 参数2:决定了参数3在哪一个线程里面执行 参数3:组内彻底下载完毕后须要执行的代码 */ dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 表示组内的全部内容所有下载完成后会来到这里 NSLog(@"把下好的视频按照顺序拼接好,而后显示在UI去播放%@", [NSThread currentThread]); }); }
1.由于你在使用的是同步的 dispatch_group_wait ,它会阻塞当前线程,因此你要用 dispatch_async 将整个方法放入后台队列以免阻塞主线程。
2.建立一个新的 Dispatch Group,它的做用就像一个用于未完成任务的计数器。
3.dispatch_group_enter 手动通知 Dispatch Group 任务已经开始。你必须保证 dispatch_group_enter 和 dispatch_group_leave 成对出现,不然你可能会遇到诡异的崩溃问题。
4.手动通知 Group 它的工做已经完成。再次说明,你必需要确保进入 Group 的次数和离开 Group 的次数相等。
5.dispatch_group_wait 会一直等待,直到任务所有完成或者超时。若是在全部任务完成前超时了,该函数会返回一个非零值。你能够对此返回值作条件判断以肯定是否超出等待周期;然而,你在这里用 DISPATCH_TIME_FOREVER 让它永远等待。它的意思,勿庸置疑就是,永-远-等-待!这样很好,由于图片的建立工做老是会完成的。
6.此时此刻,你已经确保了,要么全部的图片任务都已完成,要么发生了超时。而后,你在主线程上运行 completionBlock 回调。这会将工做放到主线程上,并在稍后执行。
7.最后,检查 completionBlock 是否为 nil,若是不是,那就运行它。
编译并运行你的应用,尝试下载多个图片,观察你的应用是在什么时候运行 completionBlock 的。
注意:若是你是在真机上运行应用,并且网络活动发生得太快以至难以观察 completionBlock 被调用的时刻,那么你能够在 Settings 应用里的开发者相关部分里打开一些网络设置,以确保代码按照咱们所指望的那样工做。只需去往 Network Link Conditioner 区,开启它,再选择一个 Profile,“Very Bad Network” 就不错。
若是你是在模拟器里运行应用,你可使用 来自 GitHub 的 Network Link Conditioner 来改变网络速度。它会成为你工具箱中的一个好工具,由于它强制你研究你的应用在链接速度并不是最佳的状况下会变成什么样。
4、定时源事件和子线程的运行循环
1 - (void)viewDidLoad { 2 [super viewDidLoad]; 3 4 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES]; 5 6 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 7 8 } 9 10 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 11 [self performSelectorInBackground:@selector(subThreadRun) withObject:nil]; 12 } 13 14 #pragma mark 15 #pragma mark - 子线程的运行循环 16 - (void)subThreadRun { 17 18 NSLog(@"%@----%s", [NSThread currentThread], __func__); 19 20 // 1.定义一个定时器 21 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES]; 22 23 // 2.将咱们的定时器加入到运行循环,只有加入到当前的运行循环里面去,他才知道你这个时候,有一个定时任务 24 /** 25 NSDefaultRunLoopMode 当拖动的时候,它会停掉 26 由于这种模式是互斥的 27 forMode:UITrackingRunLoopMode 只有输入的时候,它才会去执行定时器任务 28 29 NSRunLoopCommonModes 包含了前面两种 30 31 //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 32 //[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; 33 */ 34 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 35 36 // 下载、定时源时间、输入源时间,若是放在子线程里面,若是想要它执行任务,就必须开启子线程的运行循环 37 CFRunLoopRun(); 38 39 } 40 41 - (void)timeEvent { 42 43 NSLog(@"%d----%@", self.count, [NSThread currentThread]); 44 45 if (self.count++ == 10) { 46 NSLog(@"---挂了----"); 47 // 中止当前的运行循环 48 CFRunLoopStop(CFRunLoopGetCurrent()); 49 } 50 51 }
在完成过程当中,http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1 感谢这的提示