iOS多线程之NSThread

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

跑题了 言归正传。今天想跟大家分享下多线程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