阅读 35

iOS开发笔记(十五):多线程之 NSThread

每个 iOS 应用程序都有个专门用来更新显示UI界面、处理用户的触摸事件的主线程,因此不能将其他太耗时的操作放在主线程中执行,不然会造成主线程堵塞(出现卡机现象),带来极坏的用户体验。一般的解决方案就是将那些耗时的操作放到另外一个线程中去执行,多线程编程是防止主线程堵塞,增加运行效率的最佳方法。

iOS 支持多个层次的多线程编程,层次越高的抽象程度越高,使用也越方便,也是苹果最推荐使用的方法。下面根据抽象层次从低到高依次列出 iOS 所支持的多线程编程方法:

  1. NSThread :三种方法里面相对轻量级的,但需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销。
  2. NSOperation :是基于 OC 实现的,NSOperation 以面向对象的方式封装了需要执行的操作,不必关心线程管理、同步等问题。NSOperation 是一个抽象基类,iOS 提供了两种默认实现: NSInvocationOperation 和 NSBlockOperation,当然也可以自定义 NSOperation。
  3. Grand Central Dispatch(简称GCD,iOS4 才开始支持):提供了一些新特性、运行库来支持多核并行编程,它的关注点更高:如何在多个 CPU 上提升效率。

这篇文章简单介绍了第一种多线程编程的方式,主要是利用 NSThread 这个类,一个 NSThread 实例代表着一条线程。

线程的状态图 :

线程的状态图

1.NSthread的初始化

1.1 动态方法

// 初始化线程  
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
// 设置线程的优先级(0.0 - 1.0,1.0最高级)  
thread.threadPriority = 1;   
// 开启线程  
[thread start]; 
复制代码

参数解析

  • selector :线程执行的方法,这个 selector 最多只能接收一个参数。
  • target : selector 消息发送的对象。
  • argument : 传给 selector 的唯一参数,也可以是 nil。

线程优先级

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
复制代码

调度优先级的取值范围是 0.0 - 1.0,默认 0.5,值越大,优先级越高。这个方法的优先级的数值设置让人困惑,因为你不知道你应该设置多大的值是比较合适的,因此在 iOS8 之后推荐使用 qualityOfService 属性,通过量化的优先级枚举值来设置。

qualityOfService 的枚举值如下:

  • NSQualityOfServiceUserInteractive : 最高优先级,主要用于提供交互 UI 的操作,比如处理点击事件,绘制图像到屏幕上。
  • NSQualityOfServiceUserInitiated : 次高优先级,主要用于执行需要立即返回的任务。
  • NSQualityOfServiceDefault : 默认优先级,当没有设置优先级的时候,线程默认优先级。
  • NSQualityOfServiceUtility : 普通优先级,主要用于不需要立即返回的任务。
  • NSQualityOfServiceBackground : 后台优先级,用于完全不紧急的任务。

一般主线程和没有设置优先级的线程都是默认优先级。

1.2 静态方法

[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];  
// 调用完毕后,会马上创建并开启新线程 
复制代码

1.3 隐式创建线程的方法

[self performSelectorInBackground:@selector(run) withObject:nil]; 
复制代码

2.获取线程

//获取当前线程
NSThread *current = [NSThread currentThread];

//获取主线程
NSThread *main = [NSThread mainThread]; 
复制代码

3.线程操作

3.1 启动

使用 init 方式创建需要手动。

[thread start]; 
复制代码

3.2 睡眠

// 暂停2s  
[NSThread sleepForTimeInterval:2]; 

// 或者  
NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];  
[NSThread sleepUntilDate:date];
复制代码

3.3 取消

对于线程的取消,NSThread 提供了一个取消的方法和一个属性,调用 cancel 方法并不会立刻取消线程,它仅仅是将 cancelled 属性设置为 YES。cancelled 也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在main函数中自己实现。

- (void)cancel NS_AVAILABLE(10_5, 2_0);
复制代码

要实现取消的功能,**我们需要自己在线程的 main 函数中定期检查 isCancelled 状态来判断线程是否需要退出,当 isCancelled 为 YES 的时候,我们手动退出。**如果我们没有在 main 函数中检查 isCancelled 状态,那么调用 cancel 将没有任何意义。

3.4 退出

与充满不确定性的 cancel 相比,exit 方法可以让线程立即退出。

+ (void)exit;
复制代码

exit 属于核弹级别终极 API,调用之后会立即终止除主线程以外所有线程并退出,即使任务还没有执行完成也会中断。这就非常有可能导致内存泄露等严重问题,所以一般不推荐使用。

4.线程间的通信

在一个进程中, 线程往往不是孤立存在的, 多个线程之间需要经常的进行通信。体现在 :

  • 一个线程传递数据给另一个线程
  • 在一个线程中执行完毕特定任务后, 转到另一个线程继续执行任务

4.1 在指定线程上执行操作

//将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);

//将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);
复制代码

4.2 在主线程上执行操作

//将selector丢给主线程执行,runloop mode默认为common mode
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

//将selector丢给主线程执行,可以指定runloop mode
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array;
复制代码

4.3 在当前线程执行操作

[self performSelector:@selector(run)]; 
[self performSelector:@selector(run) withObject:nil];
[self performSelector:@selector(run) withObject:nil afterDelay:2.0]; 
复制代码

5.线程同步

线程和其他线程可能会共享一些资源,当多个线程同时读写同一份共享资源的时候,可能会引起冲突。线程同步是指是指在一定的时间内只允许某一个线程访问某个资源。

iOS 实现线程加锁有 NSLock 和 @synchronized 两种方式。

我们举一个模拟售票的例子 :

新建两个子线程(代表两个窗口同时销售门票)
NSThread * window1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
window1.name = @"北京售票窗口";
[window1 start];

NSThread * window2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
window2.name = @"广州售票窗口";
[window2 start];
复制代码

线程启动后,执行 saleTicket,执行完毕后就会退出,为了模拟持续售票的过程,我们需要给它加一个循环。

- (void)saleTicket {
	while (1) {
		//如果还有票,继续售卖
		if (_ticketCount > 0) {
			_ticketCount --;
			NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
			[NSThread sleepForTimeInterval:0.2];
		}
		//如果已卖完,关闭售票窗口
		else {
			break;
		}
	}
}
复制代码

监听线程退出的通知,以便知道线程什么时候退出。

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(threadExitNotice) name:NSThreadWillExitNotification object:nil];
复制代码

执行结果 :

2016-04-06 19:25:36.637 MutiThread[4705:1371666] 剩余票数:9 窗口:广州售票窗口
2016-04-06 19:25:36.637 MutiThread[4705:1371665] 剩余票数:8 窗口:北京售票窗口
2016-04-06 19:25:36.839 MutiThread[4705:1371666] 剩余票数:7 窗口:广州售票窗口
2016-04-06 19:25:36.839 MutiThread[4705:1371665] 剩余票数:7 窗口:北京售票窗口
2016-04-06 19:25:37.045 MutiThread[4705:1371666] 剩余票数:5 窗口:广州售票窗口
2016-04-06 19:25:37.045 MutiThread[4705:1371665] 剩余票数:6 窗口:北京售票窗口
2016-04-06 19:25:37.250 MutiThread[4705:1371665] 剩余票数:4 窗口:北京售票窗口
2016-04-06 19:25:37.250 MutiThread[4705:1371666] 剩余票数:4 窗口:广州售票窗口
2016-04-06 19:25:37.456 MutiThread[4705:1371666] 剩余票数:2 窗口:广州售票窗口
2016-04-06 19:25:37.456 MutiThread[4705:1371665] 剩余票数:3 窗口:北京售票窗口
2016-04-06 19:25:37.661 MutiThread[4705:1371665] 剩余票数:1 窗口:北京售票窗口
2016-04-06 19:25:37.661 MutiThread[4705:1371666] 剩余票数:1 窗口:广州售票窗口
2016-04-06 19:25:37.866 MutiThread[4705:1371665] 剩余票数:0 窗口:北京售票窗口
2016-04-06 19:25:37.867 MutiThread[4705:1371666] <NSThread: 0x7fdc91e289f0>{number = 3, name = 广州售票窗口} Will Exit
2016-04-06 19:25:38.070 MutiThread[4705:1371665] <NSThread: 0x7fdc91e24d60>{number = 2, name = 北京售票窗口} Will Exit
复制代码

可以看到,票的销售过程中出现了剩余数量错乱的情况,这就是前面提到的线程同步问题。

售票是一个典型的需要线程同步的场景,由于售票渠道有很多,而票的资源是有限的,当多个渠道在短时间内卖出大量的票的时候,如果没有同步机制来管理票的数量,将会导致票的总数和售出票数对应不上的错误。

我们在售票的过程中给票加上同步锁:同一时间内,只有一个线程能对票的数量进行操作,当操作完成之后,其他线程才能继续对票的数量进行操作。

- (void)saleTicket {
	while (1) {
		@synchronized(self) {
			//如果还有票,继续售卖
			if (_ticketCount > 0) {
				_ticketCount --;
				NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
				[NSThread sleepForTimeInterval:0.2];
			}
			//如果已卖完,关闭售票窗口
			else {
				break;
			}
		}
	}
}
复制代码

运行结果 :

2016-04-06 19:31:27.913 MutiThread[4718:1406865] 剩余票数:11 窗口:北京售票窗口	
2016-04-06 19:31:28.115 MutiThread[4718:1406866] 剩余票数:10 窗口:广州售票窗口	
2016-04-06 19:31:28.317 MutiThread[4718:1406865] 剩余票数:9 窗口:北京售票窗口
2016-04-06 19:31:28.522 MutiThread[4718:1406866] 剩余票数:8 窗口:广州售票窗口
2016-04-06 19:31:28.728 MutiThread[4718:1406865] 剩余票数:7 窗口:北京售票窗口
2016-04-06 19:31:28.929 MutiThread[4718:1406866] 剩余票数:6 窗口:广州售票窗口
2016-04-06 19:31:29.134 MutiThread[4718:1406865] 剩余票数:5 窗口:北京售票窗口
2016-04-06 19:31:29.339 MutiThread[4718:1406866] 剩余票数:4 窗口:广州售票窗口
2016-04-06 19:31:29.545 MutiThread[4718:1406865] 剩余票数:3 窗口:北京售票窗口
2016-04-06 19:31:29.751 MutiThread[4718:1406866] 剩余票数:2 窗口:广州售票窗口
2016-04-06 19:31:29.952 MutiThread[4718:1406865] 剩余票数:1 窗口:北京售票窗口
2016-04-06 19:31:30.158 MutiThread[4718:1406866] 剩余票数:0 窗口:广州售票窗口
2016-04-06 19:31:30.363 MutiThread[4718:1406866] <NSThread: 0x7ff0c1637320>{number = 3, name = 广州售票窗口} Will Exit
2016-04-06 19:31:30.363 MutiThread[4718:1406865] <NSThread: 0x7ff0c1420cb0>{number = 2, name = 北京售票窗口} Will Exit
复制代码

可以看到,票的数量没有出现错乱的情况。

线程通知 :

//由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次
NSString * const NSWillBecomeMultiThreadedNotification;
//这个通知目前没有实际意义,可以忽略
NSString * const NSDidBecomeSingleThreadedNotification;
//线程退出之前发送这个通知
NSString * const NSThreadWillExitNotification;
复制代码

线程的持续运行和退出 :

我们注意到,线程启动后,执行 saleTicket 完毕后就马上退出了,怎样能让线程一直运行呢,答案就是给线程加上 RunLoop。这部分与锁机制会在后面写文详细介绍。

5.优缺点

优点 : NSThread 比其他两种多线程方案较轻量级,更直观地控制线程对象。

缺点 : 需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。

参考

关注下面的标签,发现更多相似文章
评论
说说你的看法