iOS-多线程(三)NSThread
NSThread是苹果针对Pthread封装的Objective-C对象,面向对象, 简单易懂, 而且还可以直接操作线程对象;
NSThread是Foundation框架提供的最基础的多线程类,每一个NSThread对象代表一个线程;
NSThread需要自己管理线程的声明周期;
从下面几个功能点入手:
- 创建与启动线程
- 线程的状态
- 常用的属性与方法介绍
- 线程间通信
- 线程安全与同步
- 线程安全与同步示例 - 经典卖车票
1. 创建与启动线程
创建线程的几种方式:
/**
实例方法,是将target目标对象的selector,作为线程的执行任务,并且可以根据selector,来确定是否传参
初始化之后,需要调用start方法,才能将线程处于就绪状态
@param target 目标对象
@param selector 方法选择器
@param argument 方法对应的参数
@return NSThread对象
*/
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
/**
实例方法,是将block作为线程的执行任务,在iOS10才有
初始化之后,需要调用start方法,才能将线程处于就绪状态
@param block 执行任务的代码块
@return NSThread对象
*/
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/**
类方法,是将block作为线程的执行任务,直接启动线程,在iOS10才有
@param block 执行任务的代码块
*/
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/**
类方法,是将target目标对象的selector,作为线程的执行任务,并且可以根据selector,来确定是否传参
直接启动线程
@param selector 方法选择器
@param target 目标对象
@param argument 方法的参数
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
/**
创建一条后台运行的子线程,创建完线程后会自动启动线程
@param aSelector 方法选择器
@param arg 方法的参数
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
简单示例:
//1
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"1"];
[thread start];
//2
NSThread *blockThread = [[NSThread alloc] initWithBlock:^{
NSLog(@"%@:%@", @"2", [NSThread currentThread]);
}];
[blockThread start];
//3
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"3"];
//4
[NSThread detachNewThreadWithBlock:^{
NSLog(@"%@:%@", @"4", [NSThread currentThread]);
}];
//5
[self performSelectorInBackground:@selector(run:) withObject:@"5"];
//线程执行的任务
- (void)run:(NSString *)argument
{
NSLog(@"%@:%@", argument, [NSThread currentThread]);
}
打印结果:
2019-07-08 23:19:00.136694+0800 NSThreadDemo[8571:351803] 2:<NSThread: 0x600001ad4a40>{number = 4, name = (null)}
2019-07-08 23:19:00.136710+0800 NSThreadDemo[8571:351805] 4:<NSThread: 0x600001ad2940>{number = 7, name = (null)}
2019-07-08 23:19:00.136700+0800 NSThreadDemo[8571:351804] 3:<NSThread: 0x600001ad4a80>{number = 5, name = (null)}
2019-07-08 23:19:00.136752+0800 NSThreadDemo[8571:351802] 1:<NSThread: 0x600001ad4a00>{number = 3, name = (null)}
2019-07-08 23:19:00.142829+0800 NSThreadDemo[8571:351806] 5:<NSThread: 0x600001ad4ac0>{number = 6, name = (null)}
- 打印线程的number值为1的是主线程,其余的都是子线程;
- 创建线程并start后,仅仅线程的状态变为就绪状态,什么时候真正执行,需要等待CPU的调度;
2. 线程的状态
上面提到了创建线程的几种方式,其中只有两种方式返回了线程对象,所以如果你有需要控制线程的状态的话,那么只能用这两种方式进行创建线程。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
从就绪状态到运行状态是CPU调度的,无法通过代码进行触发
- 启动线程
- (void)start API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
启动线程后,线程从新建状态变成就绪状态,当线程执行的任务执行完毕后,线程就进入死亡状态,死亡过的线程不能再重新启动。(不能死而复生)
- 阻塞线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
执行后,线程进入阻塞状态,只有等待睡眠结束后,线程才会再次进入到就绪状态。
- 死亡
+ (void)exit;
退出当前线程,线程进入死亡状态,属于非正常死亡。
3. 常用的属性与方法介绍
//类属性,获取当前线程
@property (class, readonly, strong) NSThread *currentThread;
//类属性,获取主线程
@property (class, readonly, strong) NSThread *mainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//是否是主线程
@property (readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (class, readonly) BOOL isMainThread API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); // reports whether current thread is main
//以下是四个是关于线程优先级
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
//线程优先级,优先级越高被选中到执行状态的可能性越大,不能仅仅依靠优先级来判断多线程的执行顺序,不过这个已经废弃了,要使用qualityOfService
@property double threadPriority API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)); // To be deprecated; use qualityOfService below
//这是iOS8.0之后出现的
@property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0)); // read-only after the thread is started
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21, //最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
NSQualityOfServiceUserInitiated = 0x19, //次高优先级,主要用于执行需要立即返回的任务
NSQualityOfServiceUtility = 0x11, //普通优先级,主要用于不需要立即返回的任务
NSQualityOfServiceBackground = 0x09, //后台优先级,用于完全不紧急的任务
NSQualityOfServiceDefault = -1 //默认优先级
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
//线程优先级结束
//线程名称获取与设置
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//线程状态的判断
//线程是否在执行
@property (readonly, getter=isExecuting) BOOL executing API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//线程是否任务已经执行完成
@property (readonly, getter=isFinished) BOOL finished API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//线程是否已经被取消
@property (readonly, getter=isCancelled) BOOL cancelled API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
//cancel并非是退出线程,只是将上面提到的 cancelled 属性赋值为YES
- (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
这里介绍一下这个方法:
- (void)cancel API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
简单示例:
self.thread = [[NSThread alloc] initWithBlock:^{
NSThread *currentThread = [NSThread currentThread];
for (int i = 0; i < 6; ++i) {
NSLog(@"%@, cancel value=%d", currentThread, [currentThread isCancelled]);
[NSThread sleepForTimeInterval:0.5];
}
}];
[self.thread setName:@"cancel"];
[self.thread start];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.thread cancel];
});
打印结果:
2019-07-09 11:05:32.664555+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:33.167097+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:33.673121+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:34.177612+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=1
2019-07-09 11:05:34.678472+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=1
2019-07-09 11:05:35.184061+0800 NSThreadDemo[2853:56848] <NSThread: 0x600003bcdf00>{number = 3, name = cancel}, cancel value=1
可以看出调用cancel只是更改了 cancelled 属性值,并没有退出线程。
要退出线程需要用
+ (void)exit;
发送exit消息会立即终止线程任务的执行,并且退出线程。
4. 线程间通信
主要用到下方的几个方法
/**
将任务在主线程中执行
@param aSelector 方法选择器
@param arg 方法的参数
@param wait 是否阻塞当前线程等待新任务结束(结束后会继续执行后面任务)
@param array Runloop的mode
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
//默认是Runloop的modes是 kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
//参数跟上面大致是一样的,除了执行的线程可以指定。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// equivalent to the first method with kCFRunLoopCommonModes
//将任务放在子线程中执行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
简单示例-子线程下载图片,主线程显示下载完成的图片
#pragma mark - 下载图片
/**
创建一个子线程去下载图片
*/
- (void)createSubThreadToDownloadImage
{
[NSThread detachNewThreadSelector:@selector(downloadImageOnSubThread) toTarget:self withObject:nil];
}
/**
下载图片 - 子线程
*/
- (void)downloadImageOnSubThread
{
NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1562659377213&di=f9dee9bd236f21f9de550e061664ea58&imgtype=0&src=http%3A%2F%2Fres.eqxiu.com%2Fgroup1%2FM00%2FC4%2F19%2Fyq0KA1SGiReALB7PAABDN1llhBs292.png"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
//图片下载完成后,在主线程显示图片
[self performSelectorOnMainThread:@selector(showImageOnMainThread:) withObject:image waitUntilDone:NO];
}
//展示图片 - 主线程
- (void)showImageOnMainThread:(UIImage *)image
{
self.imageView.image = image;
}
5. 线程安全与同步
- 线程安全:多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的;
- 线程同步:避免线程间互相访问导致各类问题;
这部分的内容将在后面的文章中单独来学习。
6. 经典卖车票
假设有两个卖车票的窗口(A窗口,B窗口),同时卖车票,车票的总数为20张,票售完为止。
下面将通过这个例子来说明没有线程同步会出现什么问题。
#pragma mark - 卖火车票
//两个窗口 相当于 两条线程
- (void)saleTicketStart
{
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketAction) object:nil];
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketAction) object:nil];
[threadA setName:@"窗口A"];
[threadB setName:@"窗口B"];
[threadA start];
[threadB start];
}
//卖火车票 - 非线程安全
- (void)saleTicketAction
{
while ( 1 ) {
if ( self.tickets > 0 ) {
--self.tickets;
NSLog(@"%@卖了一张票,还剩下%lu张票。", [[NSThread currentThread] name], self.tickets);
} else {
NSLog(@"不好意思,票已经卖完了。");
break;
}
[NSThread sleepForTimeInterval:0.2];
}
}
打印结果:
卖票结果.png
会出现不同窗口卖票后,剩余的票数量是一样的。不考虑线程安全的情况下,得到票数是错乱的,所以我们需要考虑线程安全问题。
线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作
至于iOS实现加锁的方式有多少种,会在其他的文章中学习。
这里先使用最简单的互斥锁(@synchronized)来保证线程的安全。
while ( 1 ) {
@synchronized (self) {
if ( self.tickets > 0 ) {
--self.tickets;
NSLog(@"%@卖了一张票,还剩下%lu张票。", [[NSThread currentThread] name], self.tickets);
} else {
NSLog(@"不好意思,票已经卖完了。");
break;
}
[NSThread sleepForTimeInterval:1];
}
}
打印结果:
线程安全卖票结果.png
线程安全的情况下,加锁之后,得到的票数是正确的,没有出现混乱的情况。
这个🌰就到这里,demo传送门
。
over!