iOS多线程之1. NSThread

   多线程主要应用于与服务器进行数据传输等一些耗时操作。为了防止阻塞主线程,影响用户交互,我们必须要新建子线程来执行一些耗时操作。本文主要通过介绍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] {number = 4, name = 售票员B}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12302] {number = 5, name = 售票员C}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.848194+0800 TestAppIOS[399:12300] {number = 3, name = 售票员A}--卖了一张票,还剩余49张票
2019-03-27 15:43:03.868778+0800 TestAppIOS[399:12301] {number = 4, name = 售票员B}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872740+0800 TestAppIOS[399:12302] {number = 5, name = 售票员C}--卖了一张票,还剩余48张票
2019-03-27 15:43:03.872797+0800 TestAppIOS[399:12300] {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,这就是线程间的通信。

原理


iOS多线程之1. NSThread_第1张图片
002MKIl6zy75OmRx7rP61.jpeg

代码

// 点击屏幕开始下载图片
- (void)touchesBegan:(NSSet *)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={number = 1, name = main}
2016-11-04 13:47:04.533 TTTTTTTTTT[10584:122269] 当前线程2={number = 3, name = (null)}

  以上就是关于NSThread的大部分知识了。在开发中,我们真正应用NSThread的时候并不多,因为NSThread需要我们自己创建线程,调度任务。而线程并不是创建的越多越好,虽然多线程可以提高CPU的利用效率,但是创建多了,反而会拉低CPU运行速度,因为线程本身也需要消耗内存。所以创建几个线程,得根据CPU的当时状况来判断。这对iOS程序员来说是解决不了的问题,因为我们无法知道CPU的运行状况。所以NSThread虽然简单,但是有很多不确定的情况。我们经常使用的还是GCD和NSOperation,至于原因,后文我会详细讲解。

你可能感兴趣的:(iOS多线程之1. NSThread)