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:{number = 4, name = (null)}
2019-07-08 23:19:00.136710+0800 NSThreadDemo[8571:351805] 4:{number = 7, name = (null)}
2019-07-08 23:19:00.136700+0800 NSThreadDemo[8571:351804] 3:{number = 5, name = (null)}
2019-07-08 23:19:00.136752+0800 NSThreadDemo[8571:351802] 1:{number = 3, name = (null)}
2019-07-08 23:19:00.142829+0800 NSThreadDemo[8571:351806] 5:{number = 6, name = (null)}
  1. 打印线程的number值为1的是主线程,其余的都是子线程;
  2. 创建线程并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] {number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:33.167097+0800 NSThreadDemo[2853:56848] {number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:33.673121+0800 NSThreadDemo[2853:56848] {number = 3, name = cancel}, cancel value=0
2019-07-09 11:05:34.177612+0800 NSThreadDemo[2853:56848] {number = 3, name = cancel}, cancel value=1
2019-07-09 11:05:34.678472+0800 NSThreadDemo[2853:56848] {number = 3, name = cancel}, cancel value=1
2019-07-09 11:05:35.184061+0800 NSThreadDemo[2853:56848] {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 *)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 *)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!

你可能感兴趣的:(iOS-多线程(三)NSThread)