NSThread

NSThread

  • 实现多线程的技术方案之一.
  • 面向对象的开发思想.
  • 每个对象表示一条线程.

创建线程三种方式

准备新线程执行的方法

- (void)demo:(id)obj
{
    NSLog(@"传入参数 => %@",obj);
    NSLog(@"hello %@",[NSThread currentThread]);
}

对象方法创建

  • 实例化线程对象的同时指定线程执行的方法@selector(demo:).
  • 需要手动开启线程.
- (void)threadDemo1
{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
    // 手动启动线程
    [thread start];
}

类方法创建

  • 分离出一个线程,并且自动开启线程执行@selector(demo:).
  • 无法获取到线程对象
- (void)threadDemo2
{
    [NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}

NSObject(NSThreadPerformAdditions) 的分类创建

  • 方便任何继承自NSObject的对象,都可以很难容易的调用线程方法
  • 无法获取到线程对象
  • 自动开启线程执行@selector(demo:).
- (void)threadDemo3
{
    [self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}

总结

  • 以上三种创建线程的方式,各有不同.随意选择.
  • 使用哪种方式需要根据具体的需求而定.比如 : 如果需要线程对象,就使用对象方法创建.

target和@selector的关系

  • target : 指方法从属于的对象.
    • 比如 : 本对象--self;其他对象--self.person.
  • @selector : 指对象里面的方法.
    • 比如 : 要执行的是self中或者self.person中的哪个方法.
  • 提示 : 不要看见 target 就写 self.
  • target@selector的关系 : 执行哪个对象上的哪个方法.

代码演练

准备Person对象

@interface Person : NSObject

/// 人名
@property (nonatomic,copy) NSString *name;
/// 创建人得构造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有个方法
- (void)personDemo:(id)obj;

@end

@implementation Person

+ (instancetype)personWithName:(NSString *)name
{
    Person *person = [[Person alloc] init];
    person.name = name;
    return person;
}

- (void)personDemo:(id)obj
{
    NSLog(@"创建的人名 => %@",self.name);
    NSLog(@"hello %@",[NSThread currentThread]);
}

@end

控制器中的使用

定义属性

@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end

懒加载Person

@implementation ViewController

- (Person *)person
{
    if (_person==nil) {
        _person = [Person personWithName:@"zhangjie"];
    }
    return _person;
}

新的实例化方法

  • 使用self调用@selector(personDemo:)就会崩溃.因为self中没有@selector(personDemo:).

  • 分类方法

// 崩溃
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正确的调用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
  • 类方法
// 崩溃
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];

// 正确的调用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
  • 对象方法
// 崩溃
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];
// 正确的调用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];

线程状态

线程生命周期的控制

  • 新建
    • 内存中创建了一个线程对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
  • 就绪
    • 将线程放进可调度线程池,等待被CPU调度
[thread start];
  • 运行

    • CPU负责调度"可调度线程池"中的处于"就绪状态"的线程
    • 线程执行结束之前,状态可能会在"就绪"和"运行"之间来回的切换
    • "就绪"和"运行"之间的状态切换由CPU来完成,程序员无法干涉
  • 阻塞

    • 正在运行的线程,当满足某个条件时,可以用休眠或者来阻塞线程的执行

      • sleepForTimeInterval:休眠指定时长
      [NSThread sleepForTimeInterval:1.0];
      
      • sleepUntilDate:休眠到指定日期
      [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
      
      • 互斥锁
      @synchronized(self)
      
  • 死亡

    • 正常死亡:线程执行结束
    • 非正常死亡
      • 程序突然崩溃
      • 当满足某个条件后,在线程内部强制线程退出,调用exit方法

代码演练

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 新建状态
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
    // 就绪状态 : 将线程放进"可调度线程池",等待被CPU调度.
    [thread start];

    // 主线程中的危险操作,不能在主线程中调用该方法.会使主线程退出
//    [NSThread exit];
}
- (void)threadDemo
{
    for (int i = 0; i < 6; i++) {

        NSLog(@"%d",i);

        //1. 当前线程,每循环一次,就休眠一秒
        [NSThread sleepForTimeInterval:1.0];

        //2. 满足某一条件再次休眠一秒
        if (2==i) {
            NSLog(@"我还想再睡一秒");
            // 休眠时间为从现在开始计时多少秒以后
            [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
        }

        //3. 满足某一条件线程死亡
        if (4==i) {
            NSLog(@"线程死亡");

            // 在调用exit方法之前一定要注意释放之前由C语言框架创建的对象.
            CGMutablePathRef path = CGPathCreateMutable();
            CGPathRelease(path);

            // 线程死亡
            [NSThread exit];

            // 当线程死亡之后,以后的代码都不会被执行
            NSLog(@"线程已经死亡");
        }
    }
    NSLog(@"循环结束");
}

关于exit的结论

  • 使当前线程退出.
  • 不能在主线程中调用该方法.会使主线程退出.
  • 当前线程死亡之后,这个线程中的代码都不会被执行.
  • 在调用此方法之前一定要注意释放之前由C语言框架创建的对象.

线程属性

属性

  • name - 线程名称

    • 给线程起名字,可以方便运行调试,定位BUG
    • 在大型的商业软件中,都会设计专门的线程做特定的事情,当程序崩溃时可以快速准确的定位BUG
  • threadPriority - 线程优先级

    • 为浮点数整形,范围在0~1之间,1最高,默认0.5,不建议修改线程优先级
    • 线程的"优先级"不是决定线程调用顺序的,他是决定线程备CPU调用的频率的
    • 在开发的时候,不要修改优先级
    • 多线程开发的原则是越简单越好
  • stackSize - 栈区大小

    • 默认情况下,无论是主线程还是子线程,栈区大小都是512KB
    • 栈区大小可以设置,最小16KB,但是必须是4KB的整数倍

代码演示

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"主线程栈区空间大小 => %tu",[NSThread currentThread].stackSize/1024);

    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];

    // 给线程起名字,可以方便运行调试,定位BUG
    // 在大型的商业软件中,都会设计专门的线程做特定的事情
    thread1.name = @"download A";

    // 线程调用优先级
    // 线程的"优先级"不是决定线程调用顺序的,他是决定线程备CPU调用的频率的
    // 范围在0~1之间,1最高,默认0.5,不建议修改线程优先级
    thread1.threadPriority = 1.0;

    // 线程就绪
    [thread1 start];

    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
    thread2.name = @"download B";
    thread2.threadPriority = 0;
    [thread2 start];
}

- (void)demo
{
    NSLog(@"子线程栈区空间大小 => %tu",[NSThread currentThread].stackSize/1024);

    for (int i = 0; i < 10; i++) {
        NSLog(@"%@",[NSThread currentThread]);
    }
}

补充

  • NSInteger 有符号整数(有正负数)用 %zd
  • NSUInteger 无符号整数(没有负数)用 %tu
  • 是为了自适应32位和64位CPU的架构.
NSThread_第1张图片
占位符.png

资源共享-线程安全

多线程操作共享资源的问题

  • 共享资源

    • 资源 : 一个全局的对象、一个全局的变量、一个文件.
    • 共享 : 可以被多个对象访问.
    • 共享资源 :可以被多个对象访问的资源.比如全局的对象,变量,文件.
  • 多线程的环境下,共享的资源可能会被多个线程共享,也就是多个线程可能会操作同一块资源.

  • 当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.

  • 经典案例 : 卖票.

  • 线程安全

    • 同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的.

卖票逻辑

NSThread_第2张图片
卖票系统的简单逻辑.png

开发提示

  • 实际开发中确定开发思路逻辑比及时的写代码更重要.
  • 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码
    • 首先确保单个线程执行正确
    • 然后再添加线程

代码实现卖票逻辑

  • 先定义共享资源
@interface ViewController ()

/// 总票数(共享的资源)
@property (nonatomic,assign) int tickets;

@end
  • 初始化余票数共享资源
- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置余票数
    self.tickets = 10;
}
  • 卖票逻辑实现
-  (void)saleTickets
{
    // while 循环保证每个窗口都可以单独把所有的票卖完
    while (YES) {

        // 模拟网络延迟
        [NSThread sleepForTimeInterval:1.0];

        // 判断是否有票
        if (self.tickets>0) {
            // 有票就卖
            self.tickets--;
            // 卖完一张票就提示用户余票数
            NSLog(@"剩余票数 => %zd %@",self.tickets,[NSThread currentThread]);
        } else {
            // 没有就提示用户
            NSLog(@"没票了");
            // 此处要结束循环,不然会死循环
            break;
        }
    }
}

单线程

  • 先确保单线程中运行正常
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 在主线程中卖票
    [self saleTickets];
}

多线程

  • 如果单线程运行正常,就修改代码,实现多线程环境
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 在主线程中卖票
    // [self saleTickets];

    // 售票口 A
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread1.name = @"售票口 A";
    [thread1 start];

    // 售票口 B
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread2.name = @"售票口 B";
    [thread2 start];
}

资源抢夺结果

  • 数据错乱,数据增加.
NSThread_第3张图片
错误的卖票结果.png

出错原因分析

NSThread_第4张图片
资源共享问题分析.png

NSThread_第5张图片
资源共享问题解决.png

解决多线程操作共享资源的问题

  • 解决办法 : 使用互斥锁/同步锁.

添加互斥锁

- (void)saleTickets
{
    // while 循环保证每个窗口都可以单独把所有的票卖完
    while (YES) {
        // // 模拟休眠网络延迟
        [NSThread sleepForTimeInterval:1.0];

        // 添加互斥锁
        @synchronized(self) {
            // 判断是否有票
            if (self.tickets>0) {
                // 有票就卖
                self.tickets--;
                // 卖完一张票就提示用户余票数
                NSLog(@"剩余票数 => %zd",self.tickets);
            } else {
                // 没有就提示用户
                NSLog(@"没票了");
                // 此处要结束循环,不然会死循环
                break;
            }
        }
    }
}

互斥锁小结

  • 互斥锁,就是使用了线程同步技术.
  • 同步锁/互斥锁:可以保证被锁定的代码,同一时间,只能有一个线程可以操作.
  • self :锁对象,任何继承自NSObject的对像都可以是锁对象,因为内部都有一把锁,而且默认是开着的.
  • 锁对象 : 一定要是全局的锁对象,要保证所有的线程都能够访问,self是最方便使用的锁对象.
  • 互斥锁锁定的范围应该尽量小,但是一定要锁住资源的读写部分.
  • 加锁后程序执行的效率比不加锁的时候要低.因为线程要等待解锁.
  • 牺牲了性能保证了安全性.

原子属性

  • nonatomic : 非原子属性

  • atomic : 原子属性

    • 线程安全的,针对多线程设计的属性修饰符,是默认值.
    • 保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以读取.
    • **单写多读 : ** 单个线程写入write,多个线程可以读取read.
    • atomic 本身就有一把锁,自旋锁.
  • nonatomic和atomic对比

    • nonatomic : 非线程安全,适合内存小的移动设备.
    • atomic : 线程安全,需要消耗大量的资源.性能比非原子属性要差.
  • iOS开发的建议

    • 所有属性都声明为nonatomic,性能更高.
    • 尽量避免多线程抢夺同一块资源.
    • 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.

模拟原子属性

  • 定义属性
/// 非原子属性
@property (nonatomic,strong) NSObject *obj1;
/// 原子属性:内部有"自旋锁"
@property (atomic,strong) NSObject *obj2;
/// 模拟原子属性
@property (atomic,strong) NSObject *obj3;
  • 重写非原子属性的settergetter方法
    • 重写了原子属性的setter方法之后,会覆盖原子属性内部的自旋锁,使其失效.然后我们加入互斥锁,来模拟但写多读.
    • 重写了属性的settergetter方法之后,系统就不会再帮我们生成待下划线的成员变量.使用合成指令@synthesize,就可以手动的生成带下划线的成员变量.
// 合成指令
@synthesize obj3 = _obj3;

/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
    @synchronized(self) {
        _obj3 = obj3;
    }
}

/// obj3的getter方法
- (NSObject *)obj3
{
    return _obj3;
}

性能测试

/// 测试"非原子属性","互斥锁","自旋锁"的性能
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSInteger largeNum = 1000*1000;

    NSLog(@"非原子属性");
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj1 = [[NSObject alloc] init];
    }
    NSLog(@"非原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"原子属性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj2 = [[NSObject alloc] init];
    }
    NSLog(@"原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"模拟原子属性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj3 = [[NSObject alloc] init];
    }
    NSLog(@"模拟原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
}

测试结果

NSThread_第6张图片
num08.png

互斥锁和自旋锁对比

共同点

  • 都能够保证同一时间,只有一条线程执行锁定范围的代码

不同点

  • 互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪状态.等待被CPU重新调度.
  • 自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成.

开发建议

  • 所有属性都声明为nonatomic,原子属性和非原子属性的性能几乎一样.
  • 尽量避免多线程抢夺同一块资源.
  • 要实现线程安全,必须要用到.无论什么锁,都是有性能消耗的.
  • 自旋锁更适合执行非常短的代码.死循环内部不适合写复杂的代码.
  • 尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
  • 为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI.
  • 所有包含NSMutable的类都是线程不安全的.在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题.

异步下载网络图片

需求 : 异步下载网络图片并展示.图片可以滚动,滚动视图要是根视图.

ATS

NSAppTransportSecurity

NSAllowsArbitraryLoads


代码实现

定义属性

@interface ViewController ()

/// 滚动视图
@property (nonatomic,strong) UIScrollView *scrollView;
/// 图片视图
@property (nonatomic,weak) UIImageView *imageView;

@end

加载视图层次

  • loadView : 手动创建根视图,实现了这个方法视图控制器的view就不会从SB中加载了.当self.view==nil的时候就会调用这个方法.
- (void)loadView
{
    // 创建滚动视图
    self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 将滚动视图设置成根视图
    self.view = self.scrollView;
    self.scrollView.backgroundColor = [UIColor redColor];

    // 创建图片视图
    UIImageView *imageView = [[UIImageView alloc] init];
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

异步下载网络数据

- (void)viewDidLoad {
    [super viewDidLoad];

    // 下载网络数据
    // [self downloadImageData];

    // 开启新线程异步下载图片
    [self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}

/// 异步下载网络数据
- (void)downloadImageData
{
    // 图片资源地址
    NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
    // 所有的网络数据都是以二进制的形式传输的,所以用NSData来接受
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 设置图片视图
    // [self setupImageViewWithImage:image];

    // 回到主线程更新UI
    // waitUntilDone:是否等待主线程执行结束再执行"下一行代码",一般设置成NO,不用等待
    [self performSelectorOnMainThread:@selector(setupImageViewWithImage:) withObject:image waitUntilDone:NO];

    // 测试 waitUntilDone:
    NSLog(@"下一行代码");
}

设置图片视图

/// 设置图片视图
- (void)setupImageViewWithImage:(UIImage *)imgae
{
    NSLog(@"setupImageView");

    // 设置图片视图
    self.imageView.image = image;
    // 设置图片视图的大小跟图片一般大
    [self.imageView sizeToFit];

    // 设置滚动视图的滚动:滚动范围跟图片一样大
    [self.scrollView setContentSize:image.size];
}

strong和weak补充

  • 什么是strong,weak,分别什么时候使用?

    • strong : 强指针.
    • weak : 弱指针.
    • OC对象,根视图或者父视图用strong.
    • 子视图或者创建出来之后有强指针指向的对象用weak,
  • 提问 :

    • scrollViewimageView都用strong修饰.程序会出问题吗?
    • 连线的UI控件为什么用weak?
NSThread_第7张图片
num09.png

你可能感兴趣的:(NSThread)