ios多线程详解
一、前言
在ios中每个进程启动后都会建立一个主线程(UI进程),这个线程是其他线程的父线程。由于在ios中除了主线程,其他子线程都是独立于Cocoa Touch的。多线程的实现有以下几种方式:
NSThread:
(1)使用NSThread对象建立一个线程,非常方便。
(2)但是!使用NSThread管理多个线程非常困难,不推荐使用。
GCD--Grand Central Dispatch:
(1)基于C语言的底层API。
(2)用block定义任务,使用起来非常灵活方便。
(3)提供了更多的控制能力以及操作队列中所不能使用的底层函数。
NSOperation/NSOperationQueue:
(1)是使用GCD实现的一套Object-C的API。
(2)是面向对象的线程技术。
(3)提供了一些在GCD中不易实现的特性,如:限制最大并发数量、操作之间的依赖关系。
二、线程与进程
1.进程
进程是系统进行资源分配和调度的基本单位,每一个进程都有自己独立的虚拟内存空间。简单来说,进程是指在系统中正在运行的一个应用程序,每一个程序都是一个进程,并且进程之间是相互独立的,每个进程均运行在其专业且受保护的内存空间内。
2.线程
是程序执行流的最小单元,是系统独立调度和分派CPU的基本单位。
一个进程中至少有一条线程,即主线程。创建线程的目的就是为了开启一条新的执行路径,运行指定的代码,与主线程中的代码同时执行。
3.多线程
计算机同一个时间执行多个线程,进而提升整体处理性能。
原理:
(1)同一时间,CPU只能处理1条线程,只有一条线程在工作。
(2)多线程并发执行,其实是CPU快速的在多条线程中切换(调度)。
(3)如果CPU调度线程的速度够快,就造成多线程同时执行多假象。
优点:
(1)能适当提高程序的执行效率。
(2)能适当提高资源的利用率(CPU,内存利用率)。
缺点:
(1)开启新线程需要占用一定的内存空间(默认情况下,主线程占1M,子线程占512K)。如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
(2)线程越多,CPU在线程间切换(调度)上的开销就越大。
注:主线程栈区的1M特别宝贵。不能杀掉一个线程!但可以暂停、休眠。
三、NSThread的使用
1.线程的创建
NSThread创建线程有以下三种方法:
[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
NSThread对象的常见属性:
NSThread类方法:
(1)当前线程:
int number = [NSThread currentThread];
number == 1 表示主线程,number != 1表示后台线程
(2)阻塞方法:
休眠到指定时间
[NSThread sleepUntilDate:[NSDate date]];
休眠指定时长
[NSThread sleepForTimeInterval:4.5];
(3)其他类方法:
退出线程
[NSThread exit];
当前线程是否为主线程
[NSThread isMainThread];
是否多线程
[NSThread isMultiThreaded];
返回主线程的对象
NSThread *mainThread = [NSThread mainThread];
2.线程的状态
线程的状态如下图:
(1)新建:实例化对象
(2)就绪:向线程对象发送start消息,线程对象被加入"可调度线程池"等待CPU调度,detach方法和performSelectorInBackground方法会直接实例化一个线程对象并且加入"可调度线程池"。
(3)运行:CPU负责调度"可调度线程池"中线程的执行,线程完成执行之前,其状态可能在"就绪"和"运行"之间切换。
(4)阻塞:当满足某个条件时,可以使用休眠或锁阻塞线程执行,方法有sleepForTimeInterval,sleepUntilDate,@synchronized(self)x线程锁。线程进入阻塞状态下时,会被从"可调度线程池"中移出,CPU不再调度。
(5)死亡:死亡后线程对象的isFinished属性为YES,如果是对线程发送cancel消息,线程对象的isCenceled属性为YES,死亡后stackSize==0,内存空间被释放。
2.多线程的安全问题
多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱,从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。 UIKit中几乎所有控件都不是线程安全的,因此需要在主线程中更新UI.
解决多线程安全问题:
(1)互斥锁
注意:锁定1份代码只用1把锁,用多把锁是无效的
@synchronized(锁对象) { 需要锁定的代码 }
使用互斥锁,在同一时间,只允许一条线程执行锁中的代码。因为互斥锁的代价十分昂贵,所以锁定的代码范围应该尽可能吧小,只要锁住资源读写部分的代码即可。
(2)使用NSLock对象
(3)atomic加锁
OC在定义属性的时候有nonatomic和atomic两种选择。
atomic:原子属性,为setter方法加锁(默认就是atomic)。线程安全,但需要消耗大量资源。
nonatomic:非原子属性,不会为setter方法加锁。非线程安全,但效率高。
atomic加锁原理:
ios开发的建议:
所有属性都声明nonatomic。
尽量避免多线程抢夺同一块资源。
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动端的压力。
四、GCD的使用
GCD(Grand Central Dispatch)伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。GCD会自动利用更多的CPU内核,会自动管理线程的生命周期(线程创建,调度任务,线程销毁),只需要告诉GCD想要如何执行什么任务,不需要编写任何线程管理代码。
一些专业术语:
dispatch:调度/派遣
queue:队列,用来存放任务的先进先出(FIFO)的容器。
sync:同步函数,只是在当前线程中执行任务,不具备开启新线程的能力。
async:异步函数,可以在新的线程中执行任务,具备开启新线程的能力。
concurrent:并发,多个任务同时进行。
串行:一个任务执行完毕后,再执行下一个任务。
1.GCD中的核心概念:
任务: 任务就是要在线程中执行的操作。我们将要执行的代码用block封装好,然后将任务添加到队列容器中,并指定任务的执行方式。等待CPU从队列中取出任务放到对应的线程中执行。
队列
串行队列:一次只调度一个任务,一个任务完成后再调度下一个任务。
并发队列:可以同时调度多个任务,调度任务的方式,取决于执行任务的函数,并发功能只有在异步函数下才有效。异步情况下,开启的新线程极限数量由GCD底层决定。
如果在MRC下需要使用dispatch_release释放队列:
主队列:负责在主线程上调度任务,如果在主线程上有任务执行,会等待主线程空闲后再进行调度。主队列用于UI以及触摸事件等的操作。
全局并发队列: 由苹果API提供的,方便程序员使用多线程。
全局并发队列的优先级:
define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)优先级
注意,自定义队列的优先级都是默认优先级
define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级
全局并发队列与并发队列的区别:
(1)全局并发队列没有队列名称。
(2)在MRC中,全局并发队列不需要手动释放。
执行任务的函数
(1)同步函数(dispatch_sync)
任务被添加到队列后,队列中的任务一个接着一个执行。
在主线程中,向主队列添加同步任务,会造成死锁。
在其他线程中,向主队列添加同步任务,则会在主线程中同步执行。
(2)异步函数(dispatch_async)
GCD的其他用法
(1)延时执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});
(2)一次性执行
应用场景:保证某段代码在程序运行过程中只被执行一次,在单例模式中经常被用到。
(3)调度组(队列组)
五、NSOperation
NSOperation是苹果推荐的并发技术,它提供了一些GCD不是很好实现的功能。NSOperation是基于GCD的面向对象的OC语言封装。相比GCD,NSOperation的操作更简单。NSOperation是一个抽象类,不能直接使用,而是使用它的子类。苹果为我们提供了其两个子类:NSInvocationOperation,NSBlockOperation.以及继承NSOperation的自定义子类。
NSOperation的使用常常是配合NSOperationQueue来进行的。只要使用NSOperation子类创建的实例就能添加到NSOperationQueue中,一旦添加到队列,操作就会自动异步执行。如果没有添加到队列,而是使用start方法,则会在当前线程中执行。
(1)NSInvocationOperation
直接创建一个NSInvocationOperation对象,然后调用start方法会直接在主线程中执行。
添加到NSOperationQueue中:
(2)NSBlockOperation
NSBlockOperation与NSInvocationOperation用法相同,只是创建的方式不同,它不需要去调用方法,而是直接使用代码块。这也使得NSBlockOperation比NSInvocationOperation更流行。
(3)NSOperationQueue的一些高级操作
NSOperationQueue的高级操作有:队列的挂起,队列的取消,添加操作的依赖关系和设置最大并发数量。
最大并发数:
线程的挂起:
取消队列里的所有操作:
六、三种线程技术比较
1.NSThread
优点:NSThread比其他两个轻量级,使用简单。
缺点:需要管理自己的线程生命周期、加锁、睡眠以及唤醒等。
2.GCD
GCD是ios4.0以后才出现的并发技术
使用方式:将任务添加到队列(串行/并行(全局))中,指定执行任务的方法(同步函数sync,异步函数async)。
NSOperation无法做到的:延迟执行,队列组(NSOperation实现会比较复杂)。
3.NSOperation
NSOperation在ios2.0时就出现了(当时不好用,后苹果对其改造)
使用方式:将操作(异步执行)添加到队列(并发/全局)中。
提供了GCD不好实现的功能:最大并发数、取消所有任务、依赖关系。
GCD是比较底层的封装,我们知道较低层的代码一般性能都是比较高的,相对于NSOperationQueue。所以追求性能,而功能够用的话就可以考虑使用GCD。如果异步操作的过程需要更多的用户交互和被UI显示出来,NSOperationQueue会是一个好选择。如果任务之间没有什么依赖关系,而是需要更高的并发能力,GCD则更有优势。
尾语:
高德纳的教诲:“在大概97%的时间里,我们应该忘记微小的性能提升。过早优化是万恶之源。”只有Instruments显示有真正的性能提升时才有必要用低级的GCD。
网友评论