iOS基础 - 多线程(NSThread)
这次来介绍一下什么是多线程。大家都知道,在开发过程中应该要尽可能的减少用户的等待时间,让程序尽可能快的完成运算,提高用户体验,一款软件点击一个按钮,用户需要等待几秒钟才能进入,这款软件的用户体验就变得极差。无论是哪种语言开发的程序最终往往会转换成汇编语言进而解释成机器码来执行。但是机器码是按顺序执行的,一个复杂的多不操作只能一步步按顺序逐个执行。改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),既解决了线程阻塞又提高了运行效率。
一、进程和线程
1、进程
The term thread is used to refer to a separate path of execution for code.
进程是指在系统中正在运行的一个应用程序。每一个进程之间都是独立的,每个进程均运行在其专用且受保护的内存空间内。进程拥有独立运行的全部资源。你打开一款软件,系统就会启动一个线程提供给这款软件。一个进程想要执行任务,必须得有线程,而且每一个进程至少要有一条线程。
2、线程
The term process is used to refer to a running executable,which can encompass multiple threads.
线程是进程的基本执行单元,一个进行(程序)的所有任务都在线程中执行,比如网易云音乐,迅雷的边下边看,都是需要在线程中执行的。
3、线程的串行
一个线程中任务的执行时串行
如果要在一个线程中执行多个任务,那么只能一个一个的按顺序执行这些任务。说的通俗点:只能一个一个的按顺序执行这些任务。在同一时间内,一个线程只能执行一个任务。
二、多线程
1、什么是多线程
一个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。例如:从一个地方到达另外一个地方,有很多条路,这多条路就是多个线程,每条路上有很多的车,车就可以看成并行的任务。
2、多线程的原理
同一时间,CPU只能处理一条线程,只有一条线程是在执行的。
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象了。
因为CPU会在N多线程之间调度,会消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)
3、多线程的优缺点
优点:
能适当的提高程序的执行效率
能适当的提高资源利用率,例如:CPU、内存等
缺点:
开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能,老版本的iPhone就有可能会闪退
线程越多,CPU在调度线程上的开销就越大,程序的设计也就会更加的复杂
4、多线程一般应用
主线程的作用:显示\刷新UI界面,处理一些点击事件、滚动事件、手势等等
注意:别将比较耗时的操作放到主线程中,耗时操作会卡主主线程,严重影响UI的流畅度,给用户一种“卡”的感觉
讲完了线程与进程的基础,后面就来介绍一下在iOS中有哪些线程。iOS的多线程有3种,分别是:NSThread 、 Cocoa NSOperation 和GCD(Grand Central Dispatch),这三种编程方式从上到下,抽象层次是从低到高(抽象度越高使用越简单),也是Apple官方推荐使用的。
一、NSThread
NSThread比其他两个轻量级,但是需要自己管理线程的声明周期,线程同步。线程同步对数据的枷锁会有一定的系统开销。
// 使用NSThread创建一个线程 // 第一个参数 执行方法的对象 // 第二个参数 新线程中 绑定的方法,也就是我们需要处理的耗时操作 // 第三个参数 执行的方法中带的参数,如果不写,方法没有参数 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(printNumber:) object:@123456]; // 如果采用这个方法来创建线程对象,那么不需要我们手动执行start方法来开启线程,但是取消线程还是需要我们自己来执行方法 [NSThread detachNewThreadSelector:@selector(printNumber:) toTarget:self withObject:@123456789]; // 设定线程名称 thread.name = @"计算线程"; // 如果单独创建出一个线程对象,想让线程开启,并且执行任务,需要手动执行start方法 [thread start];
当线程执行完毕的时候,我们需要手动结束 TA。对于线程的取消,NSThread提供了一个取消的方法和一个属性。
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0); - (void)cancel NS_AVAILABLE(10_5, 2_0);
调用 cancel 方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消功能需要我们在main函数中自己实现。要实现取消的功能,我们需要自己在线程的main函数中定期检查isCancelled状态来判断线程是否需要退出,当isCancelled为YES的时候,我们手动退出。如果没有在main函数中检查isCancelled状态,那么调用 cancel 将没有任何意义。
而 exit 函数就不一样,可以让线程立即退出,而不会像 cance 这样充满不确定性。
// 不会马上结束此线程 [[NSThread currentThread] cancel]; // 立即结束此线程 [NSThread exit];
NSThread提供了2个让线程睡眠的方法,一个是根据NSDate传入睡眠时间,一个是直接传入NSTimelnterval
+ (void)sleepUntilDate:(NSDate *)date; + (void)sleepForTimeInterval:(NSTimeInterval)ti;
runloop的runUntilDate与sleepUntilDate都有阻塞线程的效果,但是阻塞之后的行为又有不一样的地方,使用的时候,我们需要根据具体需求选择合适的API。
sleepUntilDate: 相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务。
runUntilDate: 虽然会苏塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate之后的代码。
对于有runloop的线程,可以使用CFRunLoopStop()结束runloop配合cancel结束线程。
线程通信
线程准备好之后,经常需要从主线程把耗时的任务丢给辅助线程,当任务完成之后辅助线程再把结果传回主线程。这些通信一般用的都是perform方法。
//①将selector丢给主线程执行,可以指定runloop mode - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array; //②将selector丢给主线程执行,runloop mode默认为common mode - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; //③将selector丢给指定线程执行,可以指定runloop mode - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array NS_AVAILABLE(10_5, 2_0); //④将selector丢给指定线程执行,runloop mode默认为default mode - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
一般我们用③④方法将任务丢给辅助线程,任务执行完成之后再使用①②方法讲结果传回主线程。
perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行,这样的话我们就需要通过模态的方法推到主线程。
线程的优先级
由于每个线程的紧急程度是不以言给的,有的线程中的任务也许需要尽快的执行,有的线程中的任务也许并不是那么紧急,所以线程需要有优先级。优先级高的线程中的任务会比优先级低的线程先执行。
NSThread有4个优先级的API
+ (double)threadPriority; + (BOOL)setThreadPriority:(double)p; @property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below @property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started
前两个是类方法,用于设置和获取当前线程的优先级
threadPriority属性可以通过对象设置和获取优先级
由于线程优先级是一个比较抽象的东西,不知道0.5于0.6到底有多大区别,所以iOS8之后新增了qualityOfService枚举属性,大家可以通过枚举设置优先级。
NSQualityOfService主要有5个枚举值,优先级别从高到低排布。
typedef NS_ENUM(NSInteger, NSQualityOfService) { // 最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上 NSQualityOfServiceUserInteractive = 0x21, // 次高优先级,主要用于执行需要立即返回的任务 NSQualityOfServiceUserInitiated = 0x19, // 默认优先级,当没有设置优先级的时候,线程默认优先级 NSQualityOfServiceDefault = -1 // 普通优先级,主要用于不需要立即返回的任务 NSQualityOfServiceUtility = 0x11, // 后台优先级,用于完全不紧急的任务 NSQualityOfServiceBackground = 0x09, }
一般主线程和没有设置优先级的线程都是默认优先级。
主线程和当前线程
NSThread 也提供了非常方便的获取和判断主线程的API
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0); // 判断当前线程是否是主线程 + (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main // 获取主线程的thread + (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
除了获取主线程,我们也可以使用 -currentThread获取当前线程。
+ (NSThread *)currentThread;
线程通知
NSThread有三个线程相关的通知。
// 由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次 NSString * const NSWillBecomeMultiThreadedNotification; // 这个通知目前没有实际意义,可以忽略 NSString * const NSDidBecomeSingleThreadedNotification; // 线程即将退出之前发送这个通知 NSString * const NSThreadWillExitNotification;
最后,我们把上面讲到的整合起来。
// 创建线程,并制定入口main函数为 threadMain NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil]; // 设置线程的优先级,qualityOfService属性必须在线程启动之前设置,启动之后将无法再设置 self.thread.qualityOfService = NSQualityOfServiceDefault; // 调用start方法来启动线程 [self.thread start]; // 由于线程的创建非常消耗性能,大多数情况下,我们需要复用一个长期运行的线程来执行任务。
正常情况下threadMain方法执行结束之后,线程就会退出,为了线程可以长期复用接收消息,我们需要在threadMain中给thread添加runloop。
- (void)threadMain { // 设置线程的名字,这一步是不必须的,主要是为了debug的时候更方便,可以直接看出这是哪个线程出问题了 [[NSThread currentThread] setName:@"myThread"]; // 自定义的线程默认是没有runloop的,调用-currentRunLoop,方法内部会创建runloop NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // 如果没有数据源,runloop会在启动之后立刻退出。所以需要给runloop添加一个数据源,这里添加的是NSPort数据源 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; // 定期检查isCancelled,当外部调用-cancel方法将 while (![[NSThread currentThread] isCancelled]) { // 启动runloop [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; } }
线程创建好了之后我们就可以给线程丢任务了,当我们有一个需要比较耗时的任务的时候,我们可以调用perform方法将task丢给这个线程。当我们想要结束的时候就可以用CFRunLoopStop()配合-cancel来结束线程。
- (void)cancelThread { [[NSThread currentThread] cancel]; CFRunLoopStop(CFRunLoopGetCurrent()); }
不过这个方法必须在self.thread线程下调用。如果当前是主线程。可以perform到self.thread下调用这个方法结束线程