前言:本文章摘自作者devsongxx,链接:https://www.jianshu.com/p/8ff1eaebbc6e (著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。),从多线程的基本概念,进程的概念,引出iOS中的四种多线程方案pthread、NSThread、NSOperation和GCD,每一部分都有详细的代码和解释说明;在GCD中,引出同步、异步、串行队列(包括主队列)和并发队列概念,并对他们的六种组合进行详细的代码验证和说明,把这些概念安排的明明白白,然后详细的说明了GCD常见的其他用法;最后,对iOS中线程安全的方案进行全方面的介绍说明并且配上示例代码。
一、多线程概念
1、多线程概念:
一个进程中可以开启多条线程,每条线程可以并行执行不同的任务。多线程可以充分的利用多个CPU同时处理任务,提高程序的执行效率。
2、进程概念:
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的。而线程是应用程序中一条任务的执行路径。
3、进程和线程的关系:
1)一个进程可以包含多个线程;
2)一个进程中的所有任务都是在线程中执行;
4、iOS多线程实现方案:
1)pthread:纯C语言API,是一套通用的多线程API,适用于Linux、Unix、Windows等系统,线程生命周期由程序员管理。在iOS实际开发中,使用较少。
2)NSThread:使用更加面向对象,并可直接操作线程对象,线程生命周期由程序员管理,项目开发中使用较多;
3)NSOperation:基于GCD,使用更加面向对象,线程生命周期系统自动管理,使用较多;
4)GCD:一套改进的C语言多线程API,能充分利用设备的多核优势,线程生命周期系统自动管理,使用最多;
二、pThread
纯C语言API,使用较为麻烦。
图1三、NSThread
1、NSThread的创建
一个NSThread对象就代表一个线程,有三种方式创建:
1)创建线程后需要start启动线程;
2)创建线程后自动启动线程;
3)隐式创建并启动线程;
图2运行程序,打印结果:
图32、NSThread常用用法
图43、NSThread的线程通信
在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信,常用两个API如下:
图5模拟子线程下载,主线程刷新UI代码:
图6程序运行打印结果:
图7四、NSOperation
NSOperation和NSOperationQueue是对GCD的一层封装,NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类NSInvocationOperation和NSBlockOperation。
1、NSInvocationOperation
NSInvocationOperation代码:
图82、NSBlockOperation
NSBlockOperation代码:
图93、NSOperationQueue
NSOperation可以调用start方法来执行任务,但默认是同步执行的,如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行操作。
1)NSInvocationOperation与NSOperationQueue组合:
图102)NSBlockOperation与NSOperationQueue组合:
图11运行结果:Thread1、Thread2、Thread3、Thread4都是子线程,所以添加到队列之后,是异步的。
Thread4=<NSThread:0x6000020d9c80>{number=6,name=(null)}
Thread2 = <NSThread:0x6000020a3140>{number=5,name=(null)}
Thread3 = <NSThread:0x6000020dcec0>{number=7,name=(null)}
Thread1 = <NSThread:0x6000020bc740>{number=4,name=(null)}
4、控制NSOperationQueue是串行队列还是并发队列
可以通过设置maxConcurrentOperationCount的值来选择串行队列还是并发队列。
图12 图135、NSOperation之间可以设置依赖来保证执行顺序
比如一定要让操作A执行完后,才能执行操作B,代码如下:
图14 图15五、GCD
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法,它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统,它是一个在线程池模式的基础上执行的并行任务,GCD是一个替代诸如NSThread等技术的很高效和强大的技术。
GCD 会自动利用更多的 CPU 内核(比如双核、四核),GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
GCD两个核心概念,任务和队列,任务包括(同步执行任务、异步执行任务),队列包括(串行队列、并发队列),队列采用 FIFO(First In First Out)的原则,即先进先出原则。
1、同步执行、异步执行
同步执行与异步执行的区别:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
1)同步执行(sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。
2)异步执行(async):异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。
注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
2、串行队列、并发队列
串行队列和并发队列的区别:执行顺序不同,以及开启线程数不同。
1)串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(最多创建一个线程)。dispatch_get_main_queue() 主队列就是一个串行队列。
2)并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(可以开启多个线程)。dispatch_get_global_queue(0, 0) 全局队列就是并发队列。
注意:并发队列的并发功能只有在异步(dispatch_async)方法下才有效。
3、经典各种组合模式
本来同步、异步、串行、并发有四种组合,但是当前代码默认放在主队列中,全局并发队列可以作为普通并发队列来使用,所以主队列很有必要专门来研究一下,所以我们就有六种组合模式了。
1)同步执行 + 串行队列:没有开启新线程,串行执行任务。
图162)同步执行 + 并发队列:没有开启新线程,串行执行任务。
图173)异步执行 + 串行队列:有开启新线程(1条),串行执行任务。
图184)异步执行 + 并发队列:有开启新线程(多条),并发执行任务。
图195)同步执行+主队列:死锁卡住不执行。其实如果在串行队列中嵌套同步使用串行队列,也会发生死锁,原理和这个类似。所以项目中数据库处理FMDataQueue嵌套使用时,需要注意死锁问题。
如果不是在主线程执行同步执行+主队列呢,那么不会发生死锁(读者可以自己代码测试验证)。
图20运行结果:程序死锁崩溃,原因是默认viewDidLoad被添加到主队列中(先运行完viewDidLoad,后运行添加的任务),然后又同步添加Thread1任务到主队列中(先运行Thread1任务,后运行viewDidLoad任务),造成任务相互等待卡死,程序死锁崩溃。
6)异步执行+主队列:没有开启新线程,串行执行任务。
图214、GCD线程间的通信
在项目开发中,一般耗时的操作在别的线程处理,然后在主线程刷新UI,GCD线程间的通信如下:
图225、GCD其他的常见用法
1)栅栏方法(dispatch_barrier_async)
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,这就需要用到dispatch_barrier_async 方法在两个操作组间形成栅栏。NSOperation的 addDependency 也是这个效果。
图23运行结果:先执行第一组任务(Thread1、Thread2),然后再执行栅栏的第二组任务(Thread3、Thread4、Thread5)。
图242)延时方法(dispatch_after)
项目中我们有时需要几秒之后再执行某个方法,那么可以使用GCD的延时方法,而不用创建一个定时器来处理。
图25运行结果:先执行Thread1,5s之后执行Thread3,当前主队列执行完毕,然后执行dispatch_after添加的任务Thread2。至于为什么Thread3、Thread2时间接近,而不是相差2s,因为执行完Thread1的2s之后,Thread2添加到主队列,正在等待主队列执行完毕。
图263)执行一次(dispatch_once)
项目开发中,我们经常使用单例模式,那么就对 dispatch_once 很熟悉,代码只运行一次。并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。
图274)快速迭代方法(dispatch_apply)
通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法 dispatch_apply。dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部任务执行结束。如果是在串行队列中使用dispatch_apply,那么就和 for 循环一样,按顺序同步执行,所以实际使用的时候一般用并发队列。
图285)队列组(dispatch_group)
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务,这时候我们可以用到 GCD 的队列组。项目中常见使用场景有:请求多个接口数据返回后,再统一进行刷新;下载完多张图片后,再统一进行合并绘制等。
调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合来实现 dispatch_group_async。
调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
图29 图30运行结果:先异步执行group任务(Thread1、Thread2、Thread3),group执行完毕后,再执行nofity的任务。
图316)信号量(dispatch_semaphore)
项目开发中,有时候有这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
GCD 中的信号量是指Dispatch Semaphore,是持有计数的信号。在Dispatch Semaphore中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,不等待,可通过。信号量主要用于:
a、保持线程同步,将异步执行任务转换为同步执行任务;
b、保证线程安全,为线程加锁;
图32我们自己用信号量实现线程同步:
图33运行结果:semaphore初始创建时计数为 0,异步将Thread1任务添加到全局并发队列,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行,最后打印end。
图34我们自己用信号量实现 semaphore 加锁:
图35 图36运行结果:ticketCount 按照顺序依次递减1,“火车票售完”打印三次是因为有三条线程运行。
分析过程:首先三条线程都并发执行sellTickets方法,最快的一条线程首先执行dispatch_semaphore_wait,信号量减一,此时信号量为0,该条线程继续执行wait之后的代码,而其他较慢的两条线程进行等待新的信号量出现,较快的一条线程卖票之后,发送一个信号量,那么较慢的两条线程其中的一条执行wait之后的代码,如此循环,保证一个时间点只有一条线程进行卖票,从而保证线程安全。
图37如果把信号量的代码注释,运行结果:剩余票数顺序明显不对,数据错乱。
图38六、线程安全
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。线程安全问题:一般使用线程同步技术,同步技术有加锁、串行队列、信号量等。
串行队列,上文已经有介绍,比如项目中FMDBQueue,就是在串行队列中同步操作数据库,从而保证线程安全。
信号量,上文已经有介绍,其实类似于互斥锁。
下面主要讲iOS中常见的锁:
从大的方向讲有两种锁:自旋锁、互斥锁。
自旋锁:线程反复检查锁变量是否可用,是一种忙等状态,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。自旋锁的性能高于互斥锁,因为响应速度快,但是可能发生优先级反转问题(如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁)。常见的自旋锁包括:OSSpinLock、atomic等。
互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁,因此适用于线程阻塞时间较长的场合。常见的互斥锁包括:os_unfair_lock、pthread_mutex、dispatch_semaphore、@synchronized,其中pthread_mutex又衍生出NSLock、NSCondition、NSConditionLock、NSRecursiveLock,更加面向对象的锁。
网友评论