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

作者 : 开心源码 本文共7755个字,预计阅读时间需要20分钟 发布时间: 2022-05-13 共245人阅读

多线程-NSThread.png

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

说明:源码亲测,拒绝搬砖,源码可下载。
源码地址: 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.pngimage.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就先到这里吧,有任何疑问请留言,谢谢!
祝大家生活愉快!

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS详解多线程(实现篇——NSThread)

发表回复