美文网首页程序员iOS-多线程iOS Developer
iOS详解多线程(实现篇——NSThread)

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

作者: 小曼blog | 来源:发表于2020-09-27 17:03 被阅读0次
    多线程-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)

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