GCD是基于C的API,它是libdispatch的的市场名称。而libdispatch作为Apple公司的一个库,为并发代码在多核硬件(跑IOS或者OS X)上执行提供有力支持。
那么我们为什么要用GCD技术呢?
1.GCD能够推迟昂贵的计算任务,并在后台运行它们来改善你的应用的性能。
2.GCD提供一个易于使用的并发模型而不仅仅是锁和线程。以帮助我们避开并发陷阱。
3.GCD具有在常见模式(比如单例)上用更高性能的原语优化你的代码的潜在能力。
4.GCD旨在替换NSTread等线程技术。
5.GCD可充分利用设备的多核。
6.GCD可自动管理线程的生命周期。
说了这些GCD的优点,那么在实际开发中,如何使用GCD来更好满足我们的需求呢?
一、Synchronous&Asynchronous 同步&异步
1.Synchronous同步:同步任务的执行的方式:在当前线程中执行,必须等待当前语句执行完毕,才会执行下一条语句。
来看下同步的代码:
//同步的打印顺序 -(void)syncTask{ NSLog(@"begin"); //GCD的同步方法 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //任务中要执行的代码 [NSThread sleepForTimeInterval:2.0]; NSLog(@"%@",[NSThread currentThread]); }); NSLog(@"end"); }
来看打印出来的:
2019-03-29 12:00:54.993042+0800 wftest[5191:88411] begin
2019-03-29 12:00:56.994525+0800 wftest[5191:88411] <NSThread: 0x600000ff5380>{number = 1, name = main}
2019-03-29 12:00:56.994799+0800 wftest[5191:88411] end
可以看到,即使线程休眠了2秒,他依然会按照顺序执行,等代码块内的代码执行完毕后,才会执行end.
接着我们再来看异步,不在当前线程中执行,不用等当前语句执行完毕,就可以执行下一条语句
来看代码:
//异步顺序 -(void)asyncTask{ //异步不会在当前线程执行,首先需要开辟新的线程,而开辟新的线程也需要一定的时间 NSLog(@"begin"); dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%@",[NSThread currentThread]); }); NSLog(@"end"); }
我们来看下打印的情况:
2019-03-29 16:06:34.600530+0800 wftest[1336:19777] begin
2019-03-29 16:06:34.600763+0800 wftest[1336:19777] end
2019-03-29 16:06:34.600892+0800 wftest[1336:20090] <NSThread: 0x600000acc600>{number = 3, name = (null)}
可以看到,打印出来begin后,直接打印出了end.然后才执行了异步块里的代码。
接下来,我们来看看串行队列Serial queues,和并行队列(并发队列)Concurrent queues.
1.串行队列的特点:
以先进先出的方式执行,顺序调度队列中的任务执行。
无论队列中的任务函数是同步还是异步,都会等待前一个任务执行完成后,再调度后面的任务。
我们先来看看,串行队列的同步代码:
//串行队列同步函数(在一个线程中执行,注意,GCD的是C语言API,不要和OC弄混) -(void)serialSync{ //这里有两个参数,第一个参数的标识符,一般为公司域名倒写,第二个参数队列类型,DISPATCH_QUEUE_SERIAL串行,DISPATCH_QUEUE_CONCURRENT为并发队列 dispatch_queue_t serialQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL); //创建任务 void (^task1) (void) = ^(){ NSLog(@"task1---%@",[NSThread currentThread]); }; void (^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void (^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //添加任务到队列,同步执行方法 dispatch_sync(serialQueue, task1); dispatch_sync(serialQueue, task2); dispatch_sync(serialQueue, task3); }
然后来看下NSLog打印的东西:
2019-03-29 17:08:10.362074+0800 wftest[2989:50297] task1---<NSThread: 0x600002009340>{number = 1, name = main}
2019-03-29 17:08:10.364550+0800 wftest[2989:50297] task2 -- <NSThread: 0x600002009340>{number = 1, name = main}
2019-03-29 17:08:10.365860+0800 wftest[2989:50297] task3 -- <NSThread: 0x600002009340>{number = 1, name = main}
可以看到task1,taks2,task3是完全按照顺序执行的。
再来看串行队列的异步方法:
先来看代码
//串行队列异步函数 -(void)serialAsync{ //创建一个串行队列 dispatch_queue_t serialQuene = dispatch_queue_create("com.feng", DISPATCH_QUEUE_SERIAL); //2.创建任务 void (^task1)(void) = ^(){ NSLog(@"task1 --- %@",[NSThread currentThread]); }; void (^task2)(void) = ^(){ NSLog(@"task 2-- %@",[NSThread currentThread]); }; void (^task3)(void) = ^(){ NSLog(@"task 3 -- %@",[NSThread currentThread]); }; //3.添加任务队列 dispatch_async(serialQuene, task1); dispatch_async(serialQuene, task2); dispatch_async(serialQuene, task3); }
来看下打印结果:
2019-03-30 14:27:21.730761+0800 wftest[4929:84017] 主线程 -- <NSThread: 0x600000650540>{number = 1, name = main} 2019-03-30 14:27:21.731252+0800 wftest[4929:84090] task1 --- <NSThread: 0x600000638340>{number = 3, name = (null)} 2019-03-30 14:27:21.731536+0800 wftest[4929:84090] task 2-- <NSThread: 0x600000638340>{number = 3, name = (null)} 2019-03-30 14:27:21.731691+0800 wftest[4929:84090] task 3 -- <NSThread: 0x600000638340>{number = 3, name = (null)}
可以看到,串行队列异步执行,仍然按顺序执行的。也就是说,只要是串行队列,无论是异步,还是同步函数,都是按顺序执行的。
2.看完串行队列,我们来看并发(并行)队列。
并发队列的特点:
1.以先进先出的方法,并发调度队列中的任务的执行。
2.如果是并发队列的同步执行,就会等先被调度的任务执行完毕后,再执行下一个任务。
3.如果是并发队列的异步执行,同时底层线程池有可用的线程资源,会在新的任务调度后,调度下一个任务。
也就是说,先加进来的任务会先被执行,但不用等他执行完毕,就可以接着调度下一个任务。
那么,我们先来看并发队列的同步任务的代码:
//并发队列同步函数 -(void)concurrentSync{ //1.创建并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT); //2.创建任务 void (^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void (^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void (^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.添加同步任务到并发队列 dispatch_sync(concurrentQueue, task1); dispatch_sync(concurrentQueue, task2); dispatch_sync(concurrentQueue, task3); }
再来看打印情况:
2019-03-30 14:01:51.358796+0800 wftest[4073:69627] 主线程 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.359220+0800 wftest[4073:69627] task1 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.359668+0800 wftest[4073:69627] task2 -- <NSThread: 0x600001bdcd00>{number = 1, name = main} 2019-03-30 14:01:51.360195+0800 wftest[4073:69627] task3 -- <NSThread: 0x600001bdcd00>{number = 1, name = main}
可以看到,虽然是并发队列,但因为是同步任务,所以也是按顺序执行的。因为是同步任务,所以就在当前线程,主线程中执行。异步任务则会在子线程中执行。
(可以简单总结:串行 ,要等待上个任务执行完毕,才执行下个任务,所以会在同一个线程中执行。 并行:不用等上个任务执行完毕,就可以执行下个任务。同步:在当前线程中执行,不会开辟子线程。异步:在子线程中执行(这是指串行和并行队列。后面说的主队列异步,也是在主线程中执行))。
再来看并发队列的异步执行任务:
//并发队列的异步执行 -(void)concurrentAsyn{ //1.创建队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("com.feng", DISPATCH_QUEUE_CONCURRENT); //2.创建任务 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.把任务添加到队列中去 dispatch_async(concurrentQueue, task1); dispatch_async(concurrentQueue, task2); dispatch_async(concurrentQueue, task3); }
来看打印情况:
2019-03-30 14:02:45.003384+0800 wftest[4112:70392] 主线程 -- <NSThread: 0x600000ba8c40>{number = 1, name = main} 2019-03-30 14:02:45.005834+0800 wftest[4112:70450] task2 -- <NSThread: 0x600000bf7840>{number = 4, name = (null)} 2019-03-30 14:02:45.005834+0800 wftest[4112:70449] task1 -- <NSThread: 0x600000bcc380>{number = 3, name = (null)} 2019-03-30 14:02:45.005838+0800 wftest[4112:70454] task3 -- <NSThread: 0x600000bcc480>{number = 5, name = (null)}
可以看到,异步任务另外开辟了子线程。可以看到打印顺序发生了变化。
接着,我们来看全局队列。
全局队列的工作表现和并发队列一致。
但是全局队列是否就是并发队列呢?不是的。我们来看下他们的区别:
1.全局队列没有名称,无论是MRC&ARC都不用考虑释放,所以在日常开发中,建议使用全局队列。
2.并发队列:有名字,和NSThread的name属性作用类似,如果你在MRC的开发中,则需要使用dispatch_releas(q)来释放对应的对象。
那么并发队列在什么时候使用呢?在你开发第三方的框架的时候,则需要使用并发队列了。这样可以避开和使用你的开发框架的程序员弄混队列。
咱们先来看下全局队列的同步任务。(日常开发中几乎用不到。)
//全局队列的同步任务 -(void)globalSync{ NSLog(@"begin"); //1.创建全局队列 dispatch_queue_t gloabalQueue = dispatch_get_global_queue(0, 0); //2.创建任务 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //3.加入任务到队列中执行 dispatch_sync(gloabalQueue, task1); dispatch_sync(gloabalQueue, task2); dispatch_sync(gloabalQueue, task3); NSLog(@"end"); }
来看打印结果:
2019-03-30 14:35:08.160967+0800 wftest[5189:88230] 主线程 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161223+0800 wftest[5189:88230] begin 2019-03-30 14:35:08.161470+0800 wftest[5189:88230] task1 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161639+0800 wftest[5189:88230] task2 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161773+0800 wftest[5189:88230] task3 -- <NSThread: 0x600000684300>{number = 1, name = main} 2019-03-30 14:35:08.161891+0800 wftest[5189:88230] end
可以看到,按熟悉执行,同步的,都是当前线程中执行的。
再来看全局队列的异步任务,他是在子线程池上执行的,每个任务都有一个自己的线程,前提是线程池里有线程资源,底层有一个线程重用机制的。看下代码:
2019-03-30 14:39:06.429812+0800 wftest[5330:90708] 主线程 -- <NSThread: 0x600003606580>{number = 1, name = main} 2019-03-30 14:39:06.430060+0800 wftest[5330:90708] begin 2019-03-30 14:39:06.430228+0800 wftest[5330:90708] end 2019-03-30 14:39:06.430381+0800 wftest[5330:90762] task1 -- <NSThread: 0x600003660a40>{number = 3, name = (null)} 2019-03-30 14:39:06.430416+0800 wftest[5330:90763] task3 -- <NSThread: 0x600003660a00>{number = 4, name = (null)} 2019-03-30 14:39:06.430422+0800 wftest[5330:90760] task2 -- <NSThread: 0x600003660ec0>{number = 5, name = (null)}
可以看到,每个任务都有自己的独立的线程。
有点累,一会再来看看主队列。
主队列的特点:
1.专门用来在主线程上调度任务的队列。
2.不会开启子线程
3.以先进先出的方式,在主线程空闲的时候才会调度主队列中的任务在主线程中执行。
4.如果当前主线程中有任务在执行,那么无论主队列中添加了什么任务,都不会被调度。
主队列是负责在主线程中调度任务的。
会随着程序启动一起创建。
对于我们程序员来说,主队列只需要获取,不需要创建。
那么我们来看下主队列的异步任务的代码:
//主队列的异步任务 -(void)mainAsync{ NSLog(@"begin"); //1.创建主队列 dispatch_queue_main_t mainAsync = dispatch_get_main_queue(); //2.创建任务 void(^task1) (void) = ^(){ NSLog(@"task1 -- %@",[NSThread currentThread]); }; void(^task2) (void) = ^(){ NSLog(@"task2 -- %@",[NSThread currentThread]); }; void(^task3) (void) = ^(){ NSLog(@"task3 -- %@",[NSThread currentThread]); }; //添加任务到队列 dispatch_async(mainAsync, task1); dispatch_async(mainAsync, task2); dispatch_async(mainAsync, task3); NSLog(@"end"); }
来看看打印情况:
2019-04-01 16:23:52.002922+0800 wftest[2320:37104] 主线程 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.003178+0800 wftest[2320:37104] begin 2019-04-01 16:23:52.003342+0800 wftest[2320:37104] end 2019-04-01 16:23:52.092118+0800 wftest[2320:37104] task1 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.092324+0800 wftest[2320:37104] task2 -- <NSThread: 0x600001676a80>{number = 1, name = main} 2019-04-01 16:23:52.092497+0800 wftest[2320:37104] task3 -- <NSThread: 0x600001676a80>{number = 1, name = main}
大家可以注意到这里的几个情况:
1.虽然是异步的,但是三个任务仍然按顺序调度执行。
2.先执行了begin,紧接着执行的了end.然后才执行了三个任务,也就是说,主线程有空闲的时候才执行这三个任务。
我们说下deadlock死锁:是两个或者更多的线程之间出现的情况:比如第一个线程在等待第二个线程的完成才能继续执行,而第二个线程在等待第一个线程的完成才能继续执行。
看起来似乎异步更有用,效率更高,那么同步有什么用呢,我们说下同步的作用。
1.首先,同步肯定是保证了任务执行的顺序。
2.可以让后面的异步任务要依赖于某一个同步的任务。比如,必须让用户登录之后,才允许他下载电影。
我们看下代码:
//同步+异步 -(void)loadMovies{ dispatch_async(dispatch_get_global_queue(0, 0), ^{//开辟一条子线程 NSLog(@"开辟了子线程----%@",[NSThread currentThread]); dispatch_sync(dispatch_get_global_queue(0, 0), ^{ //登录,在当前的线程执行 NSLog(@"登录了---%@", [NSThread currentThread]); sleep(3); }); //2.同时下载3部电影 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]); }); }); }); }
然后我们来看打印log:
2019-04-01 17:52:46.491858+0800 wftest[5359:81060] 开辟了子线程----<NSThread: 0x600002ff8580>{number = 3, name = (null)} 2019-04-01 17:52:46.492263+0800 wftest[5359:81060] 登录了---<NSThread: 0x600002ff8580>{number = 3, name = (null)} 2019-04-01 17:52:49.497610+0800 wftest[5359:81063] 正在下载第一部电影---<NSThread: 0x600002ffaa00>{number = 4, name = (null)} 2019-04-01 17:52:49.497634+0800 wftest[5359:81062] 正在下载第三部电影---<NSThread: 0x600002ffefc0>{number = 6, name = (null)} 2019-04-01 17:52:49.497654+0800 wftest[5359:81061] 正在下载第二部电影---<NSThread: 0x600002ffef40>{number = 5, name = (null)} 2019-04-01 17:52:50.498825+0800 wftest[5359:80998] 计算机将在三秒后关闭 --<NSThread: 0x600002f9d600>{number = 1, name = main} 2019-04-01 17:52:53.752223+0800 wftest[5359:80998] 关机了---<NSThread: 0x600002f9d600>{number = 1, name = main}
我们注意到这几个方面:虽然下载电影的时候,又开启了三个新的线程,但是他们仍然要等待登录后,才能执行,以及最后,计算机回到主队列去关闭计算机的时候,也是等电影下载完毕。这是因为主队列这里的也是同步任务。前面也是同步任务。
接下来我们来看下dispatch_time的延迟操作
什么时候使用dispatch_after呢?
1.最好坚持在主队列上使用dispatch_after。而不是在自定义串行队列上,并发队列也尽量不要使用。
2.主队列(串行)是使用dispatch_after的最好选择。xcode也提供了自动完成模板。
我们来看下代码:
//延迟执行 -(void)delay{ dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); void(^task)(void)=^(){ NSLog(@"%@",[NSThread currentThread]); }; //主队列 dispatch_after(when, dispatch_get_main_queue(), task); NSLog(@"come here"); }
来看打印:
2019-04-02 10:55:47.020688+0800 wftest[2069:28686] come here 2019-04-02 10:55:49.216037+0800 wftest[2069:28686] <NSThread: 0x6000018e1c40>{number = 1, name = main}
IOS提供的一些方便使用的延迟:
//延迟执行 -(void)after{ [self.view performSelector:@selector(setBackgroundColor:) withObject:[UIColor orangeColor] afterDelay:1.0]; }
再来看看线程安全:
线程安全是多线程不可避免的问题。
dispatch_once以线程安全的方式执行,仅且执行代码一次。她会给代码设立一个临界区。试图访问临界区(即要传递给dispatch_onece的代码)的不同线程,在临界区已经有一个线程在执行的情况下会被阻塞,直到临界区完成为止。
我们来看下使用dispatch_once来实现单例线程安全:
//使用dispatch_once实现线程安全的单例 +(instancetype)sharedSingleton{ static id instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; }
如果一个单例中的单例属性是一个可变对象。那么就要考虑线程安全问题了。比如NSMutableArray。可能会出现一个线程正在读取,另外一个线程正在修改。这样就会出现线程不安全的情况。在GCD中可以通过dispatch_barrier_async来进行创建读写锁,这样一个解决方案。
接下来,我们来看下调度组(dispatch_group):
调度组的实现原理:类似引用计数,进行+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)(void) = ^(){ NSLog(@"%@----下载片头",[NSThread currentThread]); }; dispatch_group_enter(group);//引用计算+1 void (^task2) (void) = ^(){ NSLog(@"%@---下载内容",[NSThread currentThread]); [NSThread sleepForTimeInterval:3.0]; NSLog(@"----下载内容完毕"); dispatch_group_leave(group);//引用计数-1 }; dispatch_group_enter(group);//引用计数+1 void(^task3)(void)=^(){ 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.监听函数 // 参数2,表示参数3这里的代码在哪个队列中执行 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //表示组内所有的任务都完成之后,执行这里的代码 NSLog(@"把下载好的视频按顺序拼接好,然后显示在UI上播放%@",[NSThread currentThread]); }); }
看打印:
2019-04-02 16:18:45.168288+0800 wftest[12368:305146] <NSThread: 0x6000023d5fc0>{number = 4, name = (null)}---下载内容 2019-04-02 16:18:45.168288+0800 wftest[12368:305147] <NSThread: 0x6000023d5f00>{number = 3, name = (null)}----下载片头 2019-04-02 16:18:45.168288+0800 wftest[12368:305149] <NSThread: 0x6000023e9c80>{number = 5, name = (null)}----下载片尾 2019-04-02 16:18:48.174292+0800 wftest[12368:305146] ----下载内容完毕 2019-04-02 16:18:48.174604+0800 wftest[12368:305085] 把下载好的视频按顺序拼接好,然后显示在UI上播放<NSThread: 0x6000023b25c0>{number = 1, name = main}
dispatch_group_enter手动通知group任务已经开始。注意,enter和leave必须成对。否则会造成诡异崩溃问题。
最后,再来看定时源事件和子线程的运行循环:
-(void)myMain{ //1.定义一个定时器 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeEvnet) userInfo:nil repeats:YES]; //2.将定时器加入到运行循环中,只有当加入到运行循环中,他才知道这个时候,有一个定时任务 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } -(void)timeEvnet{ NSLog(@"%d----%@",self.count,[NSThread currentThread]); if(self.count++ == 10){ NSLog(@"挂了"); //停止当前的运行循环 CFRunLoopStop(CFRunLoopGetCurrent()); } }