1. 创建队列
//自己创建串行队列
dispatch_queue_t dySerial = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);
//自己创建并行队列
dispatch_queue_t dyConcurrent = dispatch_queue_create("并行队列", DISPATCH_QUEUE_CONCURRENT);
//系统默认主队列,就是主线程
dispatch_queue_t osMainSerial = dispatch_get_main_queue();
//系统默认的全局并行队列
dispatch_queue_t osConcurrent = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
2. 执行列队
//异步执行 dispatch_async("传入队列", 任务block);
//同步执行 dispatch_sync("传入队列", 任务block);
3. 不同执行 + 不同队列效果
3.1 同步执行 + 并行队列 不会创建多线程 顺序执行
3.2 异步执行 + 并发队列 会创建多个线程
3.3 同步执行 + 串行队列 不会创建新线程
3.4 异步执行 + 串行队列 只创建了一个线程(区别于当前线程的新线程)
3.5 同步/异步 + 主队列dispatch_get_main_queue()执行效果
3.5.1 特别注意: 同步执行 + 对于特殊的串行队列 dispatch_get_main_queue() 会出现死锁
首先执行任务1,然后遇到dispatch_sync 同步线程,当前线程进入等待,等待同步线程中的任务2执行完再执行任务3,这个任务2是加入到mainQueue主队列中。
dispatch_sync(osMainSerial,^(void)()) 是同步一个任务到主队列中,而当前去同步的正是主队列。因为队列的执行是FIFO(先进先出),所以两个都在等待对方完成,就会造成死锁。
3.5.2 如果这个操作放在非主队列中执行就不会有问题
3.6 异步执行 + 对于特殊的串行队列 dispatch_get_main_queue() 异步执行的时候 不会创建新线程,只会使用当前线程
4. 关于死锁
4.1 在上3.5.1上介绍 同步执行(dispatch_sync) + 主队列 = 死锁。那么如果同步执行的 + 非主队列 是否就一定不会出现死锁了?看下面的代码:
会发现还是会出现死锁的情况,综上所述不难发现这种死锁产生的原因:
在串行队列A(主队列或自己创建的串行队列)的线程中同步执行一个任务,而且这个任务还是放在串行队列A中。这样就会出现死锁。
4.2 还有一种死锁,就是当前的串行队列被阻塞,然后同步任务到这个队列就会出现
执行顺序:
main队列中死循环不结束,任务3不执行,任务3又是global全局队列同步过去的,global中同步的任务3不执行完、任务4是不会执行的。
了解死锁的发生时机后就很容易理解,所谓队列(串行并行)就是一个顺序执行的甬道。所谓同步异步就是决定执行这个队列的时候是否开辟新的线程去执行。
关于GCD之间的通信,就是一个概念记住就好。不同的队列直接相互通信,只需要在执行任务的时候同步或异步执行任务的时候调用别的队列传值
5. GCD常用方法
5.1 组操作dispatch_group
5.1.1 dispatch_group_notify、dispatch_group_wait 基本用法
异步执行多个耗时任务,然后多个任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组
dispatch_group_notify和dispatch_group_wait意思就是notify有block你可自己干某事,一个没block 干某事只能接着wait后面写顺序执行。
5.1.2 dispatch_group_enter、dispatch_group_leave 基本用法
先看一个例子
发现doSomeThing1还没执行完就进入dispatch_group_notify打印刷新页面,这是为什么了?看代码会发现doSomeThing1是放在了一个异步线程中执行的。但是dispatch_group_notify只会监听dispatch_group_async中执行的同步任务。如果dispatch_group_async执行的是个异步任务,那notify 监听结果可能不是你想要的。
那我们能不能实现dispatch_group_async执行多异步任务,同时还能监听到任务全部完成了?做到这些就需要dispatch_group_enter和dispatch_group_leave了,看下面例子:
dispatch_group_enter 用来标记这个任务的开始,dispatch_group_leave用来标记任务完成。这样成对标记可以实现标记dispatch_group_async任务的真正执行完毕,dispatch_group_notify也就能监听到了。
记住:dispatch_group_enter 和 dispatch_group_leave 是成对出现的,不然dispatch_group 队列会出错。
5.1.3 用dispatch_group 处理多网络请求都返回在刷新页面
根据5.1.2 我们可以在实际开发中运用dispatch_group 来处理多个网络请求都完成的通知。因为我们的网络请求都是异步的,正好可以用dispatch_group_enter 和 dispatch_group_leave实现标记完成
5.2 只执行一次 dispatch_once
创建单例或者有整个程序运行过程中只执行一次的代码时使用。保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。
5.3 延时执行方法:dispatch_after
在指定时间(例如3秒)之后执行某个任务。dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的
5.4 栅栏方法 dispatch_barrier_async
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
发现:任务1和任务2 全部执行完毕后 再执行追加任务,然后再执行任务3和任务4,通过这个例子我们能明白dispatch_barrier_async主要是分割异步任务的作用。
5.5 dispatch_apply 快速遍历
和一般我们用for循环遍历不同的是,如果dispatch_apply调用的时候传入queue的是并发队列,那么它的遍历就是异步执行的,会在多个线程中进行,而我们直接for循环都是在当前线程中进行的。如下
发现打印的线程号不是按顺序的,但是如果传入的是个串行队列(非主队列)它就和普通的for循环没什么不同了。如下
发现输出是按照顺序的,和for循环没什么不同,都是在主线程中进行的。
注意:如果执行dispatch_apply的线程是串行队列A的线程,同时执行dispatch_apply时传入queue的也是串行队列A,会发生死锁。比如直接在主队列中执行dispatch_apply,同时传入queue也是主队列,如:
当然这样做也没什么意义,一般dispatch_apply都是为了高效的异步的遍历数组。所以一般dispatch_apply传入的queue都是一个并发队列,同时还把dispatch_apply整体放入一个异步执行的并发队列中,如:
5.6 信号量 dispatch_semaphore
使用dispatch_semaphore 可以更好的控制并发多线程的任务处理。
5.6.1 控制并发线程数量
现在有一组图片地址,需要开辟一个并发队列来下载这些图片,需要等这些图片都下载完了才去做下面的事情
直接运行会发现会一下开辟imageArray.count个线程来处理任务。假如这里imageArray.count是100多,很显然,这样的暴力处理是不切合实际的。使用Semaphore我们可以控制同时并发的线程数量。效果如下:
发现我们先开辟5个线程去下载,5个都下载完成了后再开辟5个继续下载,这样就可以减轻系统并发的数量。
看结果你是不是感觉这有点像dispatch_barrier_async都是把任务拆成几部分一块一块执行,但是他们是有本质不同的:dispatch_barrier_async控制的是任务,它操作的对象是任务分块,不会处理线程的多少,而Semaphore控制同时并发的线程数量,再由一定的线程数量去执行任务。这好比同样是盖楼要10个人,barrier类似于我把楼分成2部分,我们10个人先盖第一部分,第一部分完了再盖第二部分。而Semaphore是我们同时只让5个人盖楼,剩下的休息,谁干完了谁休息,之前休息的接着上来干。
那如果dispatch_semaphore_create(5)改成dispatch_semaphore_create(1)会发现虽然调用的是并发队列,但是限制信号量总数为1 后,并发队列就变成了串行队列
5.6.2 对多线程并发执行任务共享资源进行 加锁处理
案例:总共有20张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
这里的问题关键是,两个窗口同时卖票,在卖出一张票后总票数都会减1,如何确保票不多卖或卖叉?这就相当于计算机中的多条线程同时写一个资源,可能会出现混乱的问题
观察发现余票在 11、6的时候出现混乱,多卖了票。这就是多线程访问公共资源出现不同步的问题。我们使用dispatch_semaphore 来加锁处理一下,看看结果怎么样?
发现总剩余票数没有发生错位。其实dispatch_semaphore这里就是控制了多线程处理任务 并发时机,也是5.1.1的一种特殊形式。
5.6.3 页面有多个网络请求都完成后才处理任务
上面说的使用dispatch_group_enter和dispatch_group_leave可以实现多网络请求完成监听处理。我们使用dispatch_semaphore也可以完成这种操作
补充: 那么如何让这些网络请求有个先后顺序了,就是说请求1返回回来的参数我要用到请求2上,这样的需求也很常见,用到的知识就不是GCD的了,而是NSOperation了
其中的[self request]里面代码还是使用信号量来控制网络请求真正结束
对于NSOperation实现多线程的方式有两种:
1.将要执行的任务封装到一个 NSOperation 对象中
2.将要执行的任务添加到一个 NSOperationQueue 对象中
NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会默认在当前队列同步执行。你也可以调用 cancel 方法在中途取消一个任务。
总结:NSOperation比GCD的好处
1、NSOperation能通过KVO键值观察者来实时监控operation的状态(是否执行,是否取消), 而GCD无法通过KVO来实时监控。通过completBlock来回调已经执行完毕。
2、NSOperation能通过设置依赖,使任务之间有先后顺序。GCD则可以通过栅栏函数来实现。
3、NSOperation能设置并行队列中任务的优先级(优先级的作用是让优先级高的线程需要cpu时能够更大几率优先得到cpu),GCD是设置不同任务队列(queue)的优先级,要实现block的优先级,需要很多代码。
4、NSOperation是将任务放入队列中来执行的。GCD是将任务放入队列中,通过同步异步函数来执行(可以多个同步异步函数)。
5、总体来说GCD更简洁
补充:
1、怎么取消正在执行的gcd任务:
可以仿照operation的工作原理,设置一个BOOL变量,当需要停止时设置成YES,执行任务的时候去判断这个状态
2、多线程使用带来的问题:资源竞争、优先倒置、死锁
以上是根据别人的博客和自己代码实践所得,如有冒犯和不足欢迎大家批评。