iOS多线程实现方案之 -- NSThread

作者: devZhang | 来源:发表于2016-07-27 23:49 被阅读2107次

    书接上回, 上次谈到iOS 多线程知识点总结之: 进程和线程, 接着就是 多线程实现方案里面的 NSThread 了.

    NSThread 多线程创建方法

    方法一: alloc init, 需要手动启动线程

        // 1. 创建线程
      NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:nil];
     
      // 2. 启动线程
      [thread start];
    
    • 通过 NSThread 调用的方法是必须只传递一个参数, 而且不一定要有返回值,在文档中是这样解释的
    selector 
    The selector for the message to send to target. This selector must take only one argument and must not have a return value.
    

    调用方法实现:

    - (void)test:(NSString *)string {
      NSLog(@"test - %@ - %@", [NSThread currentThread], string);
    }
    

    通过打印结果知此时已经创建了一个子线程(number = 2)

    test - <NSThread: 0x7fc56070cbe0>{number = 2, name = (null)} - (null)
    

    方法二: 分离子线程, 会自动启动线程

    [NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"分离子线程"];
    

    打印结果:

    test - <NSThread: 0x7ff482c12b30>{number = 2, name = (null)} - 分离子线程
    

    方法三: 开启一条后台线程, 也会自动启动线程

    [self performSelectorInBackground:@selector(test:) withObject:@"后台线程"];
    

    打印结果:

    test - <NSThread: 0x7f983960fc50>{number = 2, name = (null)} - 后台线程
    

    三种方法对比

    方法一

    • 优点: 可以拿到线程对象, 并设置相关属性
    • 缺点: 代码量相对多一点, 需要手动启动线程

    方法二和方法三

    • 优点: 创建线程简单快捷
    • 缺点: 无法拿到线程对象, 无法设置相关属性

    NSThread 常用属性设置

    NSThread 里有很多的方法和属性, 常用的有下图中的两个:



    当通过NSThread创建了不止一条线程的时候,就能用到这些了.

    name (线程名字)

    例如我们创建三条子线程,并设置子线程的name 属性

     // 创建线程A
      NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子线程"];
      threadA.name = @"子线程A";
      [threadA start];
     
      // 创建线程B
      NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子线程"];
      threadB.name = @"子线程B";
      [threadB start];
     
      // 创建线程C
      NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子线程"];
      threadC.name = @"子线程C";
      [threadC start];
    

    这样在想知道是哪条线程的时候,只需要打印鲜明名字就可以了[NSThread currentThread].name,方便查看, 打印结果如下:

    2016-07-27 14:51:20.520 多线程[75816:852836] test - 子线程A
    2016-07-27 14:51:20.520 多线程[75816:852837] test - 子线程B
    2016-07-27 14:51:20.520 多线程[75816:852838] test - 子线程C
    

    threadPriority(线程优先级)

    threadPriority 的取值范围是 0.0 -- 1.0, 默认是0.5. 数值越大, 优先级越高 ,通过代码来演示下
    这里给三个子线程设置了不同的优先级, 线程A < 线程C < 线程B

     threadA.threadPriority = 0.1;
      threadB.threadPriority = 1.0;
      threadC.threadPriority = 0.5;
    

    让三个线程都执行100次, 打印一下各个线程的运行次数和线程名字:

    for (int i = 0; i < 100; ++i) {
        NSLog(@"%d - %@", i + 1, [NSThread currentThread].name);
      }
    

    执行结果如下:


    同一时间, 三个线程的执行次数有很大差别,这是因为 线程B 的优先级最大,被执行的概率也最大, 执行次数自然也最多, 线程A 的优先级最小, 被执行的概率最小, 执行的次数自然也最小.

    NSThread 线程的生命周期

    • 只有当需要执行的任务全部执行完毕之后才会被释放掉.

    这个证明起来也很简单, 自定义一个 Thread 类继承字 NSThread , 里面重写一下 dealloc 方法, 打印一下方法名即可. 用自定义 Thread 创建一个线程, 会发现任务指向完毕之后, dealloc 方法被调用.

    线程的状态

    做了一张图


    控制线程状态

    • 启动线程
    - (void)start;
    

    线程进入就绪状态, 当线程执行完毕,进入死亡状态

    • 阻塞(暂停)线程
    + (void)sleepUntilDate:(NSDate *)date;
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    

    线程进入阻塞状态
    代码演示:

      // 阻塞线程
      //[NSThread sleepForTimeInterval:3.0];
     
      [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
    

    上面两种方法的执行效果是相同的, 开始和结束的之间线程阻塞或者说休眠了3秒

    2016-07-27 17:10:51.223 控制线程状态[84187:952380] test - <NSThread: 0x7fae6249f250>{number = 2, name = (null)}
    2016-07-27 17:10:54.231 控制线程状态[84187:952380] ----end----
    
    • 强制停止线程
    + (void)exit;
    

    线程进入死亡状态
    代码演示:
    让任务执行100次, 看下效果

    - (void)test {
     
      for (int i = 0; i < 100; ++i) {
        NSLog(@"%d - %@", i, [NSThread currentThread]);
       
      }
     
      NSLog(@"----end----");
    
    }
    

    执行完毕之后, 自动结束


    让任务在执行过程中强制停止

    - (void)test {
     
      for (int i = 0; i < 100; ++i) {
        NSLog(@"%d - %@", i, [NSThread currentThread]);
       
        if (i == 10) {
          [NSThread exit];
        }
      }
    }
    

    当达到停止条件时, 线程就强制退出了


    线程一旦进入到死亡状态, 线程也就停止了, 就不能再次启动任务.

    线程安全

    多线程的安全隐患

    • 资源共享
    1. 一块资源可能会被多个线程共享, 也就是多个线程可能会访问同一块资源
    2. 比如多个线程访问同一个对象, 同一个变量, 同一个文件
    • 当多个线程访问同一块资源时, 很容易引发数据错乱和数据安全问题

    买火车票的例子
    举这个例子, 是为了模仿我们实际 iOS 开发中可能会用到多线程下载网络数据的情况, 因为数据量可能会很大, 看是否会出现问题.

      // 火车票总数
      self.ticketCount = 100;
     
      // 三个售票员
      self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
      self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
      self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
     
      self.threadA.name = @"售票员A";
      self.threadB.name = @"售票员B";
      self.threadC.name = @"售票员C";
     
      [self.threadA start];
      [self.threadB start];
      [self.threadC start];
    

    买票方法:

    - (void)saleTicket {
     
     
      while (1) {
        NSInteger count = self.ticketCount;
        if (count > 0) {
         
          for (int i = 0; i < 1000000; ++i) {
            // 只是耗时间, 没有其他用
          }
         
          self.ticketCount = count - 1;
          NSLog(@"%@卖出一张票,还剩- %zd", [NSThread currentThread].name, self.ticketCount);
        } else {
          NSLog(@"票卖完了");
          break;
        }
       
      }
    }
    
    

    因为简单的买票操作执行非常快,无法看出效果,就在其中加了一段耗费时间的代码,这时候看到的结果是这样的



    不会出现数据混乱的情况了, 也达到了三个线程卖票的功能.

    加锁的注意点

    1. 必须是全局唯一的.
    2. 加锁的位置
    3. 加锁的前提条件(多条线程抢夺同一块资源)
      加锁的优点
    • 能有效的防止因为多线程抢夺资源造成的数据安全问题

    加锁的缺点

    • 会耗费一些额外的 CPU 资源
    • 造成线程同步(多条线程在同一条线上执行,而且是按顺序的执行)

    原子和非原子 属性

    OC 在定义属性时有 nonatomic atomic

    • atomic: 原子性, 为 setter 方法加锁(默认是 atomic)
    • nonatomic: 非原子性, 不会为 setter 方法加锁

    nonatomic 和**atomic **对比

    • atomic: 线程安全, 需要消耗大量的资源
    • nonatomic: 非线程安全, 适合内存小的移动设备

    iOS 开发建议

    • 所有属性都声明为 nonatomic
    • 尽量避免多线程抢夺同一块资源
    • 尽量将加锁, 资源抢夺的业务逻辑交给服务器端处理, 减小移动端的压力.

    线程间通信

    什么叫线程间通信
    在一个进程中, 线程往往不是孤立存在的, 多个线程之间需要经常的进行通信

    线程间通信的体现

    • 一个线程传递数据给另一个线程
    • 在一个线程中执行完毕特定任务后, 转到另一个线程继续执行任务

    线程键通信常用的方法

    - (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;
    

    例如, 给一个在 view 上的 UIImageView 添加网络图片的操作

    一般情况下,我们是直接给 imageView 设置图片

      // 网络图片 URL
      NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
     
      // 根据 URL 下载图片到本地, 保存为二进制文件
      NSData *data = [NSData dataWithContentsOfURL:url];
     
      // 转换图片格式
      UIImage *image = [UIImage imageWithData:data];
     
      // 设置图片
      self.imageView.image = image;
    

    但是如果图片比较大, 下载所需要的事件比较长, 这个时候就会造成主线程的阻塞, 影响用户体验.我们可以开启一个子线程去加载图片, 下载完毕之后再回到主线程显示图片, 这个就是线程之间的通信.

    [NSThread detachNewThreadSelector:@selector(download) toTarget:self withObject:nil];
    

    下载方法的实现:

    - (void)download {
     
      // 网络图片 URL
      NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
     
      // 根据 URL 下载图片到本地, 保存为二进制文件
      NSData *data = [NSData dataWithContentsOfURL:url];
     
      // 转换图片格式
      UIImage *image = [UIImage imageWithData:data];
     
      // 查看当前线程
      NSLog(@"download - %@", [NSThread currentThread]);
     
      // 在子线程下载后要回到主线程设置 UI
      /*
      第一个参数: 回到主线程之后要调用哪个方法
      第二个参数: 调用方法要传递的参数
      第三个参数: 是否需要等待该方法执行完毕再往下执行
      */
      [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    }
    

    设置并显示图片方法实现:

    - (void)showImage:(UIImage *)image {
      // 设置图片
      self.imageView.image = image;
      // 查看当前线程
      NSLog(@"showImage - %@", [NSThread currentThread]);
    }
    

    控制台打印结果是:



    可以看到,下载图片是在子线程, 设置图片是回到了主线程操作的
    关于回到主线程设置图片, 除了上面提到的方法,还是使用

    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
    

    这个也是需要调用 showImage 方法,效果一样.

    也可以直接使用self.imageView调用performSelectorOnMainThread: withObject: waitUntilDone:方法, 这样不需要再去生命一个showImage方法, 就可以回到主线程设置图片.

    [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    

    也能达到我们想要的效果.
    这也是线程间通信最常用的情景.

    • 关于 NSThread 多线程的总结就到这里, 下篇将对 GCD 进行总结学习

    相关文章:
    iOS 多线程知识点总结之 -- 进程和线程
    iOS多线程实现方案之--GCD

    相关文章

      网友评论

      本文标题:iOS多线程实现方案之 -- NSThread

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