iOS详解多线程(实现篇——NSThread)

多线程-NSThread.png

上一节中,我们详细的学习了和多线程有关的概念,像进程、线程、多线程、CPU内核、并发、并行、串行、队列、同步、异步等概念。这一节中,我们将用代码来实现多线程。
如果对多线程概念不太清楚的,可以参考上一节内容,链接如下:
详解多线程(概念篇——进程、线程以及多线程原理)

说明:源码亲测,拒绝搬砖,源码可下载。
源码地址:https://github.com/weiman152/Multithreading.git

在iOS中,多线程的实现方法有多种,有OC的也有C语言的,有常用的,也有不常用的。本节中,我们就先探究NSThread这个OC的类对于实现多线程是如何进行的。

多线程的实现方法

  1. NSThread(OC)
  2. GCD(C语言)
  3. NSOperation(OC)
  4. C语言的pthread(C语言)
  5. 其他实现多线程方法
1.NSThread(OC)

NSThread是苹果提供的面向对象的操作线程的方法。简单方便,可以直接操作线程对象。
我们查看一下NSThread的API,发现内容并不多,属性和方法不是特别多,我们一个个来看看(根据字面意思理解的)。
注:不想看的可以跳过哟,直接到下面看代码。
先看看类的声明:

image.png

NSThread继承自NSObject。

  • currentThread
    声明的第一个属性,currentThread,当前上下文所在的线程。这也是我们非常常用的一个属性。


    image.png
  • 类方法创建线程


    image.png
  • isMultiThreaded 判断是否有多个线程


    image.png
  • threadDictionary 线程字典
    每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。

image.png
  • 让当前线程阻塞一段时间


    image.png
  • 退出线程


    image.png
  • 线程的优先级


    image.png
  • 这几个字面上看不出来干嘛的


    image.png
  • 线程的名字


    image.png
  • 栈的大小


    image.png
  • 是否是主线程和获取主线程


    image.png
  • 初始化线程


    image.png
  • 线程状态(正在执行、结束、被取消)


    image.png
  • 线程主函数 在线程中执行的函数 都要在-main函数中调用,自定义线程中重写-main方法


    image.png
  • 线程有关的通知


    image.png

上面的API都是我根据字面意思理解的,不一定正确,下面我们就用代码来试验一下NSThread实现多线程的过程吧。

1》类方法创建子线程,并在子线程中执行想要的操作

//类方法创建线程
- (IBAction)createThreadC:(id)sender {
    NSLog(@"------------detachNewThreadWithBlock-------");
    //block创建,并在子线程进行想要的操作
   [NSThread detachNewThreadWithBlock:^{
       NSLog(@"--block--%@",[NSThread currentThread]);
    }];
    NSLog(@"------------detachNewThreadSelector-------");
    //在子线程中执行某方法
    [NSThread detachNewThreadSelector:@selector(printHi) toTarget:self withObject:nil];
}

-(void)printHi {
    NSLog(@"---printHi---");
    NSLog(@"Hi, 我要在子线程中执行");
    NSLog(@"--Sel--%@",[NSThread currentThread]);
}

打印结果:


image.png

分析:
createThreadC在主线程中,因为开辟子线程需要耗费时间,所以会先打印主线程的:
------------detachNewThreadWithBlock-------
------------detachNewThreadSelector-------
然后在打印子线程的内容。因为子线程是并发的,谁先执行完并不确定,所以先打印哪个子线程的内容也是不确定的。
注意:如果主线程和子线程都有一个for循环,循环很多次,那么主线程和子线程中的for循环打印很可能是交叉进行的。

我们再次运行,看看结果是否与上次一样呢。


image.png

与上次结果不太一样哟,与我们上面的分析是一致的。

2》判断当前是否开启了多个线程 isMultiThreaded

我们分别在子线程和主线程中使用isMultiThreaded,看看结果:


image.png

子线程中:


image.png

打印结果:


image.png

结果是 YES,就是开启了多线程。我们把开启的子线程注释掉再看看。


image.png

看看打印结果:


image.png

结果也是1,也是YES,这是为什么呢?
多方搜索,也没有找到答案。
我想,因为我是在一个应用程序中,应用程序默认开启主线程,是不是应用程序默认还开启了别的线程?我们看一下系统的CPU占用情况:


程序刚启动CPU占用情况.png

上图是程序程序刚启动的时候CPU的使用情况,我们并没有开启线程,但是系统却开启了5个线程,并且线程2是有使用的,所以我们打印是否开启了多线程的时候,会是YES。
我们静置了一会儿,再看看系统的线程情况:


image.png

现在就剩下线程1和线程8了。
我们自己开启了线程之后,看看CPU中线程开启情况:


image.png

在图中我们找到了我们自己创建的线程一,编号为12 。
现在,我们明白了,为什么在应用程序中打印 [NSThread isMultiThreaded]结果为什么一直是YES了。

那么,我们新建一个控制台项目,打印看看:


image.png

果然,打印是0,也就是NO,认为没有多个线程。

3》是否是主线程,打印主线程

- (void)viewDidLoad {
  [super viewDidLoad];
  
  NSLog(@"000  %d", [NSThread isMultiThreaded]);
  NSLog(@"isMainThread: %d", [NSThread isMainThread]);
  NSLog(@"currentThread: %@", [NSThread currentThread]);
}

image.png

4》对象方法创建子线程

对象方法初始化子线程,我们可以得到一个子线程对象,然后使用这个子线程对象。如果我们要开启子线程,一定要调用start方法,不然线程是不会开启的。

- (IBAction)createThreadO:(id)sender {
    NSLog(@"新建多线程");
    //对象方法创建多线程 一
    self.thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"thread1: %@",[NSThread currentThread]);
        for (int i=0; i<100; i++) {
            NSLog(@"i= %d", i);
            [NSThread sleepForTimeInterval:1];
        }
    }];
    self.thread1.name = @"线程一";
    //对象方法创建多线程 二
    NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
    thread2.name = @"线程二";
    [thread2 start];
}

-(void)hello:(NSString *)name {
    NSLog(@"你好!%@",name);
    NSLog(@"当前线程是: %@",[NSThread currentThread]);
}

看看打印结果:


image.png

因为线程一没有开启,只是初始化了,所以不会执行线程一的内容。
使用对象方法创建子线程,要想让线程执行,必须调用start方法开启子线程。

5》取消线程——cancel,并不能取消一个子线程

我们在NSThread中找到一个方法叫做cancel,看起来像是可以取消一个线程,我们来试一试。

- (IBAction)createThreadO:(id)sender {
    NSLog(@"新建多线程");
    //对象方法创建多线程 一
    self.thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"thread1: %@",[NSThread currentThread]);
        for (int i=0; i<10000; i++) {
            NSLog(@"i= %d", i);
        }
    }];
    self.thread1.name = @"线程一";
    
    //对象方法创建多线程 二
    NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
    thread2.name = @"线程二";
    [thread2 start];
}

- (IBAction)threadStart:(id)sender {
    NSLog(@"thread1开始");
    [self.thread1 start];
}

- (IBAction)threadCancel:(id)sender {
    NSLog(@"thread1 取消");
    [self printState:self.thread1];
    [self.thread1 cancel];
    NSLog(@"cancel 后:");
    [self printState:self.thread1];
    if([self.thread1 isCancelled]==YES){
        NSLog(@"thread1 被取消了,开始销毁它");
        [NSThread exit];
        self.thread1 = nil;
    }
}

执行后发现,根本不能取消,线程还是在执行完循环之后才停止的。我们看看该方法的官方文档:
Instance Method
cancel
Changes the cancelled state of the receiver to indicate that it should exit.

意思是说,这个方法只是把cancelled的属性置为YES,并不能真正的取消当前线程。

看看打印结果:


image.png

我们要想取消一个子线程,只是使用cancel是做不到的,cancel只是把属性isCancelled设置为YES,并不能真正的取消一个子线程。我们可以配合isCancelled属性,使用类方法exit,取消一个子线程。
注意:上面我们的案例中,由于使用的是按钮取消,按钮方法是在主线程中进行的,在主线程中执行exit是不会有效果的。所以,在这种状态下,我们的线程一是不能被取消的。要想取消线程一,我们需要在子线程内部进行。
例如:

//再次测试取消线程
- (IBAction)cancelThreadAgain:(id)sender {
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

- (void)run {
    NSLog(@"当前线程%@", [NSThread currentThread]);

    for (int i = 0 ; i < 100; i++) {
        NSLog(@"i = %d", i);
        if (i == 20) {
            //取消线程
            [[NSThread currentThread] cancel];
            NSLog(@"取消线程%@", [NSThread currentThread]);
        }

        if ([[NSThread currentThread] isCancelled]) {
            NSLog(@"结束线程%@", [NSThread currentThread]);
            //结束线程
            [NSThread exit];
            NSLog(@"这行代码不会打印的");
        }

    }
}

看看结果:


image.png

只打印了前20个数字,说明线程取消了。

网上有人说,如果在线程中使用了sleep方法,就不能取消线程了,我们试一试:


image.png

看看结果:


image.png

跟之前一样,还是可以取消的。说明sleep是不会影响线程的取消退出操作的。

6》线程状态

使用NSThread创建的子线程,我们可以得到线程的三个状态:是否结束、是否取消、是否正在执行


image.png
-(void)printState:(NSThread *)thread{
    NSLog(@"状态,isCancelled: %d",[thread isCancelled]);
    NSLog(@"状态,isFinished: %d",[thread isFinished]);
    NSLog(@"状态,isExecuting: %d",[thread isExecuting]);
}

7》让线程阻塞一段时间

有的时候,我们希望线程等待一会儿再执行,这个时候,我们可以使用
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
这两个方法,让线程阻塞一会儿在执行。观察后发现,这两个方法也是类方法,那么我们调用的时候,会阻塞当前线程,还是把所有线程都阻塞呢?我们试一试吧。

- (IBAction)sleepAction:(id)sender {
    NSThread * threadA = [[NSThread alloc] initWithBlock:^{
        //threadA 阻塞2秒后执行
        [NSThread sleepForTimeInterval:2.0];
        for (int i=0; i<10; i++) {
            NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
        }
        NSLog(@"threadA 结束了");
    }];
    threadA.name = @"线程A";
    [threadA start];
    
    NSThread * threadB = [[NSThread alloc] initWithBlock:^{
        for (int i=0; i<10; i++) {
            NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
        }
        NSLog(@"threadB 结束了");
    }];
    threadB.name = @"线程B";
    [threadB start];
    
}

打印结果:


image.png

先打印了线程B的内容,说明sleep方法并不会阻塞所有的线程,只会阻塞当前的线程。

另一个方法传入一个日期类型,也就是等到某一个特殊日期的时候才会执行。

    //让这个线程等到某个日期的时候在执行,这里给的是当前时间的2秒后执行,只是为了测试。
    [NSThread detachNewThreadWithBlock:^{
        NSDate * date = [NSDate dateWithTimeIntervalSinceNow:2];
        [NSThread sleepUntilDate:date];
        NSLog(@"终于等到这一天啦!我执行啦!");
    }];

结果:


image.png

8》案例:售票问题
描述:
假如我们有三个售票员ABC同时都在售票,每售出一张票,就从库存中减去一张,直到所有的票售完。

我们用代码去模拟这个过程。
分析一下:三个售票员我们用三个线程模拟,设置总票数为100,每个线程都执行一个总票数减1的操作,直到总票数为0 。

实现代码如下:

//售票
- (IBAction)sellTickets:(id)sender {
    self.totalTickets = 100;
    
    NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t1.name = @"售票员:王美美";
    [t1 start];
    
    NSThread * t2 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t2.name = @"售票员:李帅帅";
    [t2 start];
    
    NSThread * t3 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t3.name = @"售票员:张靓靓";
    [t3 start];
}

- (void)sell{
    NSLog(@"开始售票,当前余票:%d", self.totalTickets);
    while (self.totalTickets > 0) {
        [NSThread sleepForTimeInterval:1.0];
        self.totalTickets--;
        NSLog(@"%@ 卖出一张,余票:%d", [NSThread currentThread].name, self.totalTickets);
    }
}

看看打印结果:


image.png
image.png

我们发现,结果并不像我们预期的那样啊,输出有点错乱,而且居然出现了-1,这实在是不能容忍的。
为什么会出现这样的问题呢?
因为三个线程同时访问我们的公共资源self.totalTickets,当线程一访问了,还没有减1的时候,线程二或者线程三也进来访问了,这个时候,线程二或者线程三读取的还是之前的self.totalTickets,所以就会出现打印两次甚至三次相同余票的情况。
为了解决这个问题,我们在线程访问公共资源的时候加个锁,也就是说,当线程一准备访问公共资源的时候,我们就把公共资源锁住,不让其他线程进来。当线程一访问完了,再进行解锁,其他线程继续访问。
代码如下:

- (void)sell{
    NSLog(@"开始售票,当前余票:%d", self.totalTickets);
    while (self.totalTickets > 0) {
        [NSThread sleepForTimeInterval:1.0];
        //互斥锁--锁内的代码在同一时间只有一个线程在执行
        @synchronized (self) {
            if(self.totalTickets > 0){
                self.totalTickets--;
                NSLog(@"%@ 卖出一张,余票:%d", [NSThread currentThread].name, self.totalTickets);
            }else{
                NSLog(@"余票不足,出票失败!");
            }
            
        }
    }
}

为了尽快打印,所以把总票数改成10张。
看看打印结果:


image.png

解决了问题。

NSThread小结:
NSThread是官方提供的,面向对象的创建多线程的方法。

  1. NSThread可以使用类方法快速创建子线程,但是得不到子线程对象,线程自动开启。
  2. NSThread可以使用对象方法创建子线程,能够得到子线程对象,但是要手动开启子线程。
  3. NSThread可以取消子线程、可以随时查看线程的状态(正在执行、被取消、结束)。
  4. NSThread可以随时查看当前代码所在的线程。

关于NSThread就先到这里吧,有任何问题请留言,谢谢!
祝大家生活愉快!

你可能感兴趣的:(iOS详解多线程(实现篇——NSThread))