美文网首页iOS-多线程
iOS多线程之NSThread

iOS多线程之NSThread

作者: 土鳖不土 | 来源:发表于2018-10-14 00:09 被阅读53次

    国庆之后真的很糟糕,有时候在深夜偷偷的问自己
    是不是不适合写代码?
    是不是不适合当程序员?
    是不是要转行才能见到未来?
    是不是这辈子就打工下去。。。

    跑题了 言归正传。今天想跟大家分享下多线程NSThread 接下来也会有

    iOS多线程之GCD
    iOS多线程之NSOperation
    iOS多线程之NSRunLoop

    三篇陆续和大家见面

    什么是NSThread?

    一个NSThread对象代表一个线程,需要手动管理线程的生命周期,处理线程同步等问题 看下官网的一张介绍图:


    image.png

    翻译下:

    如果希望在自己的执行线程中运行Objective-C方法,请使用此类。 当您需要执行冗长的任务但不希望它阻止执行其余应用程序时,线程特别有用。 特别是,您可以使用线程来避免阻塞应用程序的主线程,该主线程处理用户界面和与事件相关的操作。 线程还可用于将大型作业划分为多个较小的作业,这可能会导致多核计算机的性能提升。NSThread类支持类似于NSOperation的语义,用于监视线程的运行时条件。 您可以使用这些语义来取消线程的执行,或确定线程是否仍在执行或已完成其任务。 取消线程需要线程代码的支持; 有关详细信息,请参阅取消说明。

    实例方法创建、启动线程

    // 1. 创建线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    还有这两种方式:
        /*
         NSThread  *thread = [[NSThread alloc]init];
         NSThread  *thread = [[NSThread alloc]initWithBlock:^{
         NSLog(@"thread");
         }];
         */
    
    // 2. 启动线程
    [thread start];    // 线程一启动,就会在线程thread中执行self的run方法
    
    // 新线程调用方法,里边为需要执行的任务
    - (void)run {
    NSLog(@"%@", [NSThread currentThread]);
    }
    

    类方法 创建线程后自动启动线程

    // 1. 创建线程后自动启动线程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
    
    // 新线程调用方法,里边为需要执行的任务
    - (void)run {
    NSLog(@"%@", [NSThread currentThread]);
    }
    
    

    隐式创建并启动线程

    // 1. 隐式创建并启动线程
    [self performSelectorInBackground:@selector(run) withObject:nil];
    
    // 新线程调用方法,里边为需要执行的任务
    - (void)run {
    NSLog(@"%@", [NSThread currentThread]);
    }
    

    常用的一些属性

     //设置线程名字
        [thread setName:@"thread - 1"];
        //设置线程优先级
        [thread setThreadPriority:1.0];
        //IOS 8 之后 推荐使用下面这种方式设置线程优先级
        //NSQualityOfServiceUserInteractive:最高优先级,用于用户交互事件
        //NSQualityOfServiceUserInitiated:次高优先级,用于用户需要马上执行的事件
        //NSQualityOfServiceDefault:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
        //NSQualityOfServiceUtility:普通优先级,用于普通任务
        //NSQualityOfServiceBackground:最低优先级,用于不重要的任务
        [thread setQualityOfService:NSQualityOfServiceUtility];
        //判断线程是否是主线程
        [thread isMainThread];
        //线程状态
        //是否已经取消
        [thread isCancelled];
        //是否已经结束
        [thread isFinished];
        //是否正在执行
        [thread isExecuting];
    

    线程启动、取消、暂停

    //线程开始
        [thread start];
        //线程取消
        [thread cancel];
        //线程暂停
        [thread sleepForTimeInterval:1.0];
        //或者下面这种方式 让线程休眠1秒
        [thread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
        //立即终止除主线程以外所有线程
        [NSThread exit];
    

    线程之间的通信

    在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法

    // 在主线程上执行操作
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
    // equivalent to the first method with kCFRunLoopCommonModes
    
    // 在指定线程上执行操作
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
    
    // 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
    - (id)performSelector:(SEL)aSelector;
    - (id)performSelector:(SEL)aSelector withObject:(id)object;
    - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    

    接下来 我在举个例子 就是很常见的线程间的通信,在子线程里下载图片在主线程刷新UI

    /**
     * 创建一个线程下载图片
     */
    - (void)downloadImageOnChildThread {
        // 在创建的子线程中调用downloadImage下载图片
        [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
    }
    
    /**
     * 下载图片,下载完之后回到主线程进行 UI 刷新
     */
    - (void)downloadImage {
        NSLog(@"current thread -- %@", [NSThread currentThread]);
        
        // 1. imageUrl URL格式化
        NSURL *imageUrl = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1539450809191&di=eed853766615f765d622e08803b4e8a9&imgtype=0&src=http%3A%2F%2Ffile03.16sucai.com%2F2017%2F1100%2F16sucai_p20161106032_0c2.JPG"];
        
        // 2. 从 imageUrl 中读取数据(下载图片)
        //-- 耗时操作
        NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
        // 通过二进制 data 创建 image
        UIImage *image = [UIImage imageWithData:imageData];
        
        self.imageView.image = image;    
        // 3. 回到主线程进行图片赋值和界面刷新
    //    [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
    
    }
    
    /**
     * 回到主线程进行图片赋值和界面刷新
     */
    - (void)refreshOnMainThread:(UIImage *)image {
        // 赋值图片到imageview
        self.imageView.image = image;
    }
    
    

    以上代码会crash


    image.png

    设置图片必须在主线程中执行,也就是UI必须在主线程中刷新

        // 3. 回到主线程进行图片赋值和界面刷新
        [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
    

    加上这句就OK了


    image.png

    丝滑的刷新出来了

    线程安全和线程同步

    线程安全:

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

    线程同步:

    可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
    接下来举两个例子 一个是线程不安全一个是线程安全的

    场景:总共有20张体育彩票,有两个售卖体育彩票的窗口,一个是篮球彩票售卖窗口,另一个是足球彩票售卖窗口。两个窗口同时售卖体育彩票,卖完为止。

    线程不安全

    /**
     * 初始化体育彩票票数量、卖票窗口(非线程安全)、并开始卖票
     */
    - (void)initTicketStatusNotSave {
        // 1. 设置剩余彩票为 50
        self.ticketSurplusCount = 50;
        
        // 2. 篮球彩票票票窗口的线程
        NSThread  *salebBasketballWindow  = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        salebBasketballWindow.name = @"篮球彩票票票窗口";
        
        // 3. 足球彩票售票窗口的线程
        NSThread  *salebFootballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        salebFootballWindow.name = @"足球彩票售票窗口";
        
        // 4. 开始售卖体育彩票
        [salebBasketballWindow start];
        [salebFootballWindow start];
        
    }
    
    /**
     * 售卖体育彩票(非线程安全)
     */
    - (void)saleTicketNotSafe {
        while (1) {
            //如果还有票,继续售卖
            if (self.ticketSurplusCount > 0) {
                self.ticketSurplusCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            //如果已卖完,关闭售票窗口
            else {
                NSLog(@"所有彩票均已售完");
                break;
            }
        }
    }
    
    
    image.png

    可以观察以上的数据是错乱的

    线程安全

    /**
     * 初始化体育彩票票数量、卖票窗口(非线程安全)、并开始卖票
     */
    - (void)initTicketStatusSave {
        // 1. 设置剩余彩票为 50
        self.ticketSurplusCount = 50;
        
        // 2. 篮球彩票票票窗口的线程
        NSThread  *salebBasketballWindow  = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        salebBasketballWindow.name = @"篮球彩票票票窗口";
        
        // 3. 足球彩票售票窗口的线程
        NSThread  *salebFootballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
        salebFootballWindow.name = @"足球彩票售票窗口";
        
        // 4. 开始售卖体育彩票
        [salebBasketballWindow start];
        [salebFootballWindow start];
        
    }
    
    /**
     * (线程安全)
     */
    - (void)saleTicketSafe {
        while (1) {
            // 互斥锁
            @synchronized (self) {
                //如果还有票,继续售卖
                if (self.ticketSurplusCount > 0) {
                    self.ticketSurplusCount --;
                    NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                    [NSThread sleepForTimeInterval:0.2];
                }
                //如果已卖完,关闭售票窗口
                else {
                    NSLog(@"所有彩票均已售完");
                    break;
                }
            }
        }
    }
    
    
    image.png

    OK 了
    解决线程安全就是给线程枷锁,也就是在 在一个线程执行该操作的时候,不允许其他线程进行操作
    iOS中的线程锁有很多:
    官方上的有:


    image.png

    @synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get

    这里用:synchronized

    线程的状态转换

    当线程创建时

     // 1. 创建线程
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    初始化
     // 2. 启动线程
        [thread start]; 
    
    就绪状态
    就绪状态
    • 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。

    • 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。

    • 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。


      这张图可以很好的解释线程的状态切换

    总结:

    NSThread属于轻量级多任务实现方式,可以更加只管的管理线程,需要管理线程的生命周期、同步、加锁问题,会导致一定的性能开销,
    注:以上有些素材从网上查找,如有侵权望告知修改。如有不对望指正谢谢。

    自勉:“即便是只有上班的命 也要有上天的心”

    传送门:
    https://github.com/tubie/JFMultiThreading.git

    相关文章

      网友评论

        本文标题:iOS多线程之NSThread

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