当新打开一个APP的时候,系统会新创建一个进程。这个进程会默认新创建一个线程,把这个线程命名为主线程。多线程主要应用于与服务器进行数据传输等一些耗时操作。为了防止阻塞主线程,影响用户交互,我们必须要新建子线程来执行一些耗时操作。本文主要通过介绍NSThread的使用方法,来探讨线程的生命周期、线程安全,线程间通信。
1.线程的生命周期
线程的生命周期分为:1.创建线程;2.调度任务;3.销毁线程。一个NSThread对象就是一条线程,获得一个NSThread对象的方式有两种。
- (void)viewDidLoad {
[super viewDidLoad];
// 方式1
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
//调度任务,例如下载图片,往服务器上传文件等一切耗费时间的操作
NSLog(@"thread1--------执行任务");
}];
// 开始执行线程中的任务,相当于调度任务
[thread1 start];
//方式 2
//argument:id类型,为向方法(executeTask:)中传递的参数
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(executeTask:) object:@{@"key" : @"value"}];
[thread2 start];
}
- (void)executeTask:(id)argument
{
/*
thread2--------执行任务-------argument = {
key = value;
}
**/
NSLog(@"thread2--------执行任务-------argument = %@",argument);
}
创建一个NSThread对象并往线程中添加了任务之后,必须执行[thread start];
才会执行线程中的任务。[thread start];
只能执行一次,否则会报attempt to start the thread again
的错误。
当线程中的任务执行完之后,系统会自动执行[NSThread exit]
销毁线程,释放内存。在销毁线程之前,[NSThread exit]
这个方法会发送一个通知NSThreadWillExitNotification
通知观察者线程即将销毁。由于通知的发出是同步的,所以回调的执行在线程销毁之前。所以我们可以用下面的方法来监测线程的销毁。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
//调度任务,例如下载图片,往服务器上传文件等一切耗费时间的操作
NSLog(@"thread1--------执行任务");
}];
// 开始执行线程中的任务,相当于任务调度
thread1.name = @"thread1";
[thread1 start];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillExit:) name:NSThreadWillExitNotification object:nil];
}
- (void)threadWillExit:(NSNotification *)noti
{
// 日志: thread1 马上退出了
NSLog(@"%@ 马上退出了",[[NSThread currentThread] name]);
}
NSThread提供了很多属性和方法,方便我们使用。下面主要介绍常用的几个属性和方法。
// 类属性,获取当前线程 [NSThread currentThread]
@property (class, readonly, strong) NSThread *currentThread;
// 类属性,获取主线程 [NSThread mainThread]
@property (class, readonly, strong) NSThread *mainThread;
//线程的名字
@property (nullable, copy) NSString *name;
2.线程安全
一块数据如果被多个线程同时访问,就容易发生数据错乱和数据安全问题。下面举一个卖票的例子。
- (void)viewDidLoad {
[super viewDidLoad];
// 一共50张票
self.ticketNum = 50;
// 三个售票员同时开始卖票
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread1.name = @"售票员A";
self.thread1 = thread1;
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread2.name = @"售票员B";
self.thread2 = thread2;
NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread3.name = @"售票员C";
self.thread3 = thread3;
[thread1 start];
[thread2 start];
[thread3 start];
}
- (void)sellTickets
{
while (1) {
int count = self.ticketNum;
//1.先检查票数
if (count > 0) {
//暂停一段时间
[NSThread sleepForTimeInterval:0.02];
//2.票数-1
self.ticketNum = count-1;
//获取当前线程
NSThread *current= [NSThread currentThread];
NSLog(@"%@--卖了一张票,还剩余%d张票",current,self.ticketNum);
}
if (self.ticketNum == 0){
//退出线程
[NSThread exit];
}
}
}
因为日志太长,仅贴出部分日志。
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票员B}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票员C}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848194+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票员A}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.868778+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票员B}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872740+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票员C}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872797+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票员A}--卖了一张票,还剩余48张票
通过日志打印我们可以看出,一共50张票,但是ABC每个售票员都卖了50张票,这明显是不对的。那问题出在哪里呢?三个线程同时访问门票的数量(共享数据),发生了数据错乱。
1.售票员A查询门票数量的时候,发现门票还有50张,卖了一张,还剩49张。
2.售票员B查询门票数量的时候(此时售票员A还没有把门票卖出去),发现门票还有50张,卖了一张,还剩49张。
3.售票员C查询门票数量的时候(此时售票员A.B还没有把门票卖出去),发现门票还有50张,卖了一张,还剩49张。
4.这就出现了一个怪现象,每个售票员都卖出1张票,却还剩49张门票的原因。
那怎样解决这个问题呢?一个售票员卖票的时候,其他两个人等着。等这个售票员卖完了,这俩售票员其中的一个再卖。
- (void)sellTickets
{
while (1) {
// 加一把锁
@synchronized (self) {
int count = self.ticketNum;
//1.先检查票数
if (count > 0) {
//暂停一段时间
[NSThread sleepForTimeInterval:0.02];
//2.票数-1
self.ticketNum = count-1;
//获取当前线程
NSThread *current= [NSThread currentThread];
NSLog(@"%@--卖了一张票,还剩余%d张票",current,self.ticketNum);
}
}
if (self.ticketNum == 0){
//退出线程
[NSThread exit];
}
}
}
除了@synchronized
,还有NSLock
也可以达到相同的效果,解决多线程资源共享产生的数据安全问题。@synchronized
就是对括号里的代码加锁,一个线程执行完了之后,另一个线程才能执行。(售票员A卖票呢,此时BC不能卖。等A卖完了,BC才能卖。BC再去查询票的数量的时候就变成了49张(因为A卖了一张))。
锁完美解决了多线程带来的数据安全问题,不过锁需要消耗大量的CPU资源,所以我们开发的时候尽量避免这种场景。
3.线程间通信
理论上讲线程都不是孤立存在的,需要相互传递消息。最常见的就是我们把一些耗时操作放在子线程,例如下载图片,但是下载完毕我们不能在子线程更新UI,因为只有主线程才可以更新UI和处理用户的触摸事件,否则程序会崩溃。此时,我们就需要把子线程下载完毕的数据传递到主线程,让主线程更新UI,这就是线程间的通信。
原理
代码
// 点击屏幕开始下载图片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"当前线程1=%@",[NSThread currentThread]);
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"当前线程2=%@",[NSThread currentThread]);
NSString *strURL = @"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg";
UIImage *image = [self downloadImageWithURL:strURL];
if (image) {
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
}
}];
[thread start];
}
日志
2016-11-04 13:47:04.532 TTTTTTTTTT[10584:122182] 当前线程1=<NSThread: 0x600000260c80>{number = 1, name = main}
2016-11-04 13:47:04.533 TTTTTTTTTT[10584:122269] 当前线程2=<NSThread: 0x600000265d80>{number = 3, name = (null)}
以上就是关于NSThread的大部分知识了。在开发中,我们真正应用NSThread的时候并不多,因为NSThread需要我们自己创建线程,调度任务。而线程并不是创建的越多越好,虽然多线程可以提高CPU的利用效率,但是创建多了,反而会拉低CPU运行速度,因为线程本身也需要消耗内存。所以创建几个线程,得根据CPU的当时状况来判断。这对iOS程序员来说是解决不了的问题,因为我们无法知道CPU的运行状况。所以NSThread虽然简单,但是有很多不确定的情况。我们经常使用的还是GCD和NSOperation,至于原因,后文我会详细讲解。