第二部分 NSThread
先看一段API文档的描述
An NSThread object controls a thread of execution. Use this class when you want to have an Objective-C method run in its own thread of execution. Threads are especially useful when you need to perform a lengthy task, but don’t want it to block the execution of the rest of the application. In particular, you can use threads to avoid blocking the main thread of the application, which handles user interface and event-related actions. Threads can also be used to divide a large job into several smaller jobs, which can lead to performance increases on multi-core computers.
大概的意思是:一个NSThread对象管理一个线程的执行。当你想要将一个Objective-C方法运行在它自己独立的线程中,可以使用这个类。当你想执行一个比较耗时(冗长)的操作而又不想阻塞程序其他部分的运行状态时,线程是特别有用的。尤其是你可以使用线程来避免阻塞主线程处理用户界面以及和事件相关的活动。线程可以将待处理任务分割成小任务以提高多核计算机的性能。
一、NSThread的使用
1.线程的创建
方式一:
/ * 创建并启动线程
*
* 参数1要执行的方法
* 参数2提供selector的对象,通常是self
* 参数3传递给selector的参数
*/
[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]
方式二:
//参数一:提供selector的对象,通常是self,参数2:要执行的方法,参数3:传递给selector的参数(如果selector方法不带参数,就使用nil)
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];
方式三:
//隐式创建并启动线程,第一个参数为调用的方法,第二个参数为传给selector方法的参数
- (void)performSelectorInBackground:(SEL)aSelector
withObject:(id)arg
NSThread对象的常见属性
//只读属性,线程是否在执行
thread.isExecuting;
//只读属性,线程是否被取消
thread.isCancelled;
//只读属性,线程是否完成
thread.isFinished;
//是否是主线程
thread.isMainThread;
//线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优 //先级,优先级高,CPU调度的频率高
thread.threadPriority;
//线程的堆栈大小,线程执行前堆栈大小为512K,线程完成后堆栈大小 为0K
//注意:线程执行完毕后,由于内存空间被释放,不能再次启动
thread.stackSize;
NSThread对象的方法
//线程开始,线程加入线程池等待CPU调度(并非真正开始执行,只是通常等待时间都非常短,看不出效果)
[thread start];
if(!thread.isCancelled){//在执行之前需要先确认线程状态,如果已经取消就直接返回
[thread cancel]; //通知线程取消,可以在外不终止线程执行
}else{
return;
}
2.NSThread的类方法
类方法都用在线程内部,也就是说类方法作用于包含本行类方法的线程。
<1>当前线程,在开发中常用于调试,适用于所有多线程计数,返回一个线程号码
//number == 1 表示主线程,number != 1表示后台线程
int number = [NSThread currentThread];
<2>阻塞方法
//休眠到指定时间
[NSThread sleepUntilDate:[NSDate date]];
//休眠指定时长
[NSThread sleepForTimeInterval:4.5];
<3>其他类方法
//退出线程
[NSThread exit];
//当前线程是否为主线程
[NSThread isMainThread];
//是否多线程
[NSThread isMultiThreaded];
//返回主线程的对象
NSThread *mainThread = [NSThread mainThread];
3.线程的状态
<1>新建:实例化对象
<2>就绪:向线程对象发送 start 消息,线程对象被加入“可调度线程池”等待 CPU 调度;detach 方法和 performSelectorInBackground 方法会直接实例化一个线程对象并加入“可调度线程池”
<3>运行:CPU 负责调度“可调度线程池”中线程的执行,线程执行完成之前,状态可能会在“就绪”和“运行”之间来回切换,“就绪”和“运行”之间的状态变化由 CPU 负责,程序员不能干预
<4>阻塞:当满足某个预定条件时,可以使用休眠或锁阻塞线程执行,影响的方法有:sleepForTimeInterval,sleepUntilDate,@synchronized(self)x线程锁;
线程对象进入阻塞状态后,会被从“可调度线程池”中移出,CPU 不再调度
<5>死亡
死亡方式
正常死亡:线程执行完毕
非正常死亡:线程内死亡--->[NSThread exit]:强行中止后,后续代码都不会在执行
线程外死亡:[threadObj cancel]--->通知线程对象取消,在线程执行方法中需要增加 isCancelled 判断,如果 isCancelled == YES,直接返回
死亡后线程对象的 isFinished 属性为 YES;如果是发送 calcel 消息,线程对象的 isCancelled 属性为YES;死亡后 stackSize == 0,内存空间被释放。
4.多线程的安全问题
多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才能按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。
解决多线程安全问题
<1>互斥锁
// 注意:锁定1份代码只用1把锁,用多把锁是无效的
@synchronized(锁对象) { // 需要锁定的代码 }
使用互斥锁,在同一个时间,只允许一条线程执行锁中的代码.因为互斥锁的代价非常昂贵,所以锁定的代码范围应该尽可能小,只要锁住资源读写部分的代码即可。使用互斥锁也会影响并发的目的。
<2>原子属性
@property (strong, nonatomic) UIWindow *window;
atomic:能够实现“单写多读”的数据保护,同一时间只允许一个线程修改属性值,但是允许多个线程同时读取属性值,在多线程读取数据时,有可能出现“脏”数据 - 读取的数据可能会不正确。原子属性是默认属性,如果不需要考虑线程安全,要指定 nonatomic。
atomic(原子属性)在setter方法内部加了一把自旋锁
nonatomic(非原子属性)下,set和get方法都不会加锁,消耗资源小适合内存小的移动设备
UIKit中几乎所有控件都不是线程安全的,因此需要在主线程上更新UI
原子属性内部使用的 自旋锁
自旋锁和互斥锁的区别
共同点: 都可以锁定一段代码。 同一时间, 只有线程能够执行这段锁定的代码
区别:互斥锁,在锁定的时候,其他线程会睡眠,等待条件满足,再唤醒
自旋锁,在锁定的时候, 其他的线程会做死循环,一直等待这条件满足,一旦条件满足,立马去执行,少了一个唤醒过程
// 在主线程更新UI,有什么好处?
- 只在主线程更新UI,就不会出现多个线程同时改变 同一个UI控件
- 主线程的优先级最高。也就意味UI的更新优先级高。 会让用户感觉很流畅
开发建议
1.所有属性都声明为nonatomic
2.尽量避免多线程抢夺同一块资源
3.尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
5.自动释放池和运行循环
<1>运行循环
作用:保证程序不退出,坚挺所有事件,例如:手势触摸,网络加载等
特性:没有事件时,会休眠(省电),一旦监听到事件,会立即响应,每一个线程都有一个 runloop,但是只有主线程的 runloop 会默认启动。
<2>自动释放池
工作原理:自动释放池被销毁或耗尽时会向池中所有对象发送 release 消息,释放所有 autorelease 的对象!
创建和销毁:每一次运行循环启动后会创建自动释放池;程序执行过程中,所有 autorelease 对象在出了作用域之后,会被添加到最近创建的自动释放池中;运行循环结束前,会释放自动释放池。
自动释放池在ARC中同样需要。
工作原理图:
常见面试题:
int largeNumber = 2 * 1024 * 1024;
// 问题:(1)以下代码是否存在问题?(2)如果有,怎么修改?
for (int i = 0; i < largeNumber; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"Hello "];
str = [str uppercaseString];
str = [str stringByAppendingString:@" - World"];
}
}
网上的解决办法:
1)@autoreleasepool 放在外面,保证循环之后释放循环中的自动释放对象
2)@autoreleasepool 放在内部,每一次循环之后,都倾倒一次自动释放池,内存管理是最好的,但是性能不好!
6.线程通信(方法继承自NSObject)
//在主线程上执行操作,例如给UIImageVIew设置图片
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
//在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wai
iOS多线程系列之一:多线程基础
iOS多线程系列之二: NSThread
iOS多线程系列之三:GCD
iOS多线程系列之四:NSOperation以及多线程技术比较