美文网首页iOS
iOS-多线程(三)NSThread

iOS-多线程(三)NSThread

作者: 厦门_小灰灰 | 来源:发表于2019-07-10 11:28 被阅读0次

    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)}
    
    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] <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!

    相关文章

      网友评论

        本文标题:iOS-多线程(三)NSThread

        本文链接:https://www.haomeiwen.com/subject/tqezhctx.html