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

书接上回, 上次谈到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;
    }
   
  }
}

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


显然, 是有问题的, 多次出现卖出同一张票的情况. 也就是造成了数据混乱. 那该怎么解决呢? 这个时候就要用到 -- 互斥锁.

- (void)saleTicket {
  while (1) {
    // 加 互斥锁, 全局唯一, self, 代表锁对象
    @synchronized(self) {
      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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 一: 多线程的基本概念1.同步与异步的概念1.1 同步 必须等待当前语句执行完毕,才可以执行下一个语句。1.2...
    程序_猿阅读 3,167评论 1 17
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 在2013年的时候每个月都会写总结,2014年没有这么做,主要是因为和工作相关的内容不能公开,而其他方面又没有量化...
    清风捷影阅读 220评论 0 1
  • 2017微信公开课,由于朋友提前已经购票没有时间,临时由我代为参加,因此,昨天很偶然的机会,在现场有幸聆听了张小龙...
    乐趣先生阅读 288评论 0 0
  • 其实泰迪熊对约翰来说是童年,泰迪和约翰所做的很多事情,打雷时约翰和泰迪一起唱歌,这是他们从小的习惯。泰迪从...
    会读书的猫cat阅读 780评论 0 0