线程
queue是队列,thread是线程。不开启新线程就是当前queue无效,任务的执行还是在这个方法所在的线程里不变。还有主队列并不特殊
,只是它的主线程创建时机是在main()之前,由系统早就创建好了,不会像自定义串行队列一样在使用到的异步执行的时候才去创建。所以异步执行+串行子队列会开启一个线程,但异步执行+主线程就不会开启新的,因为早就开好了主线程。
对 微内核Mach 封装
的宏内核 BSD
为了更好地利用多核,加入了工作队列,以支持多核多线程处理,这也是 GCD 能更高效工作的基础
。
GCD底层就是copy和封装要执行的任务block,设置回调函数func,因为队列也是对象,所以这里会递归执行到根类,将block dx_push
,用pthread_create
创建线程,然后dx_invoke
执行block。
总结: queue队列也是个对象,有父类,根类,也需要alloc init,且传入的serial或concurrent就决定了它初始化时候宽度为1还是Max。它的isa指向的OS_dispatch##_queue_serial是宏定义拼接而成的.创建队列在底层的实现是通过模板创建的。
-
dispatch_async异步执行函数: 将任务拷贝并封装,设置回调函数func,底层通过dx_push递归,会重定向到根队列,通过pthread_creat创建线程,最后通过dx_invoke执行block回调。
-
dispatch_sync同步执行函数:同步函数的底层实现其实是同步栅栏函数barrier,栅栏函数只对自定义的并发队列有效。先将任务push队列中,然后执行block回调,在将任务pop,所以任务是顺序执行的。
-
栅栏函数只对自定义的并发队列有效,因为全局并发队列还会有系统自己的函数去调用,阻塞住全局并发队列可能会崩溃,阻塞串行队列本身就是有序同步 此时加栅栏,会浪费性能。同步栅栏函数会阻塞线程,即{}之外的执行会阻塞,异步栅栏函数阻塞的是队列,即{}里的,外面的不受影响,深刻理解,即 dispatch_barrier_async(queue, ^{后执行});NSLog(@“先执行")。
栅栏函数对异步网络请求这种不同线程进行通信就很难受,信号量、调度组leave-enter-notify就可以解决。
-
单例只执行一次,简单来说就是传入的具有唯一性的静态变量在底层做了原子性封装关联,并通过此关联得到任务是否执行过的状态。如果任务没有执行过,会加锁,加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回。
-
将任务压入队列: _dispatch_thread_frame_push
-
执行任务的block回调: _dispatch_client_callout
-
将任务出队:_dispatch_thread_frame_pop
dispatch_barrier
dispatch_sync
的底层其实就是GCD的栅栏化barrier。且dispatch_barrier
在串行队列上发挥不了作用。栅栏函数只对自定义的并发队列
有效,串行队列本身底层就是栅栏,所以等于脱裤子放屁,而且更耗性能。如果栅栏函数对全局并发队列,则可能会崩溃,因为全局并发队列不止是自己在调用,系统也在调用,如果barrier阻塞了系统函数,就会异常,即如果对全局并发队列执行dispat_barrier_sync,会崩溃。如果对全局并发队列执行dispat_barrier_async,则无效,起不到阻塞作用,需要是自定义的并发队列。
NSThread
NSThread 是苹果官方提供的,可以直接操作线程对象,控制start
、阻塞sleepUntilDate
、强制停止exit
。当调用[thread start];后,系统把线程对象放入可调度线程池中,线程对象进入就绪状态。需要自己维护线程的生命周期(主要是创建)、线程之间同步等。
NSOperation
NSOperation、NSOperationQueue
是基于 GCD 更高一层的封装,完全面向对象。可以方便的控制线程,比如取消
线程、暂停
线程、设置线程的优先级
、设置线程的依赖
,所以多用于下载库
的实现。
NSOperation的子类NSBlockOperation
和NSInvocationOperation
可以实现不同的回调方式。NSOperationQueue
的优势是
- 可以添加操作之间的
依赖关系
、 - 设定操作执行的
优先级
,高优先级的操作不一定先执行,需要看操作是否在就绪
状态。当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。 - 设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行
- 可以方便取消一个操作的执行,实质是标记 isCancelled 状态。这里的取消并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
GCD
GCD 是 Apple 开发的一个多核编程的较新的解决方法。GCD的优势是
- 会自动利用更多的 CPU 内核
- 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
应用
dispatch_once
dispatch_barrier_async
dispatch_after
dispatch_group_notify
dispatch_semaphore
死锁
死锁:系统发现此线程既需要等待又需要执行,不知道怎么执行,所以抛出了_crash异常。
GCD 死锁的充分条件是:“向当前
队列重复同步
提交 block”。提交的 block 阻塞了队列,而队列阻塞后永远无法执行完dispatch_sync(),可见这里完全和代码所在的线程无关
。
数据竞争Data race
Data Race是指多个线程在没有正确加锁的情况下,同时访问同一块数据
,并且至少有一个线程是写操作,对数据的读取和修改产生了竞争,从而导致各种不可预计的问题。
可以在xcode的scheme的 Diagnostics页面, 选中Thread Sanitizer,来更好的追踪此类问题。
如何解决这种Data race问题呢? 将共享变量的 read和write放在同一个DispatchQueue中. 采用什么样的DispatchQueue, 这里有2种方法,推荐第二种:
- 采用串行的DispatchQueue, 所有的read/write都是串行的, 所以不会出现Data race的问题; 但是效率比较低,即使所有的操作都是read, 也必须排队一个一个的读.
- 采用
并行
的DispatchQueue, 所有的read
都可以并行进行, 所有的write
都必须"独占"(barrier
)的进行: 我write的时候, 任何人不允许read或者write.
自定义线程池
为什么要构建线程池?
答:大量的任务提交到后台队列时,某些任务会因为某些原因被锁住导致线程休眠,或者被阻塞,concurrent queue 随后会创建新的线程来执行其他任务。当这种情况变多时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。CPU 是用时间片轮转来实现线程并发的,尽管 concurrent queue 能控制线程的优先级,但当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。使用 concurrent queue 时不可避免会遇到这种问题,但使用 serial queue 又不能充分利用多核 CPU 的资源。工具 YYDispatchQueuePool,为不同优先级创建和 CPU 数量相同的 serial queue,每次从 pool 中获取 queue 时,会轮询返回其中一个 queue。我把 App 内所有异步操作,包括图像解码、对象释放、异步绘制等,都按优先级不同放入了全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题。
那么构建一个线程池,需要3个参数:名称、线程数量、优先级
每种优先级各对应n个队列。获取队列queue的时候就从创建好的这n个队列轮询返回给用户使用,一般n定位5个左右即可,视使用场景而定。
为什么要使用队列数组(每个队列是同步队列)来实现队列池呢?
- 其一,创建系统激活的核数大小的队列数组,是为了充分利用多核的效率,同时也尽可能的减少时间片的切换带来的cpu消耗的影响。
- 其二,在dispatch_async中使用队列,意味这创建了对应这么多的异步线程,同时每个线程中队列是同步队列,保证了不会创建更多的线程,同时任务在异步操作中每个任务间是同步操作的
网友评论