NSThread

作者: 月下独酌灬 | 来源:发表于2016-04-28 11:44 被阅读4154次

    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<UITouch *> *)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<UITouch *> *)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的架构.
    占位符.png

    资源共享-线程安全

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

    • 共享资源

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

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

    • 经典案例 : 卖票.

    • 线程安全

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

    卖票逻辑

    卖票系统的简单逻辑.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<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        // 在主线程中卖票
        [self saleTickets];
    }
    

    多线程

    • 如果单线程运行正常,就修改代码,实现多线程环境
    - (void)touchesBegan:(NSSet<UITouch *> *)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];
    }
    

    资源抢夺结果

    • 数据错乱,数据增加.
    错误的卖票结果.png

    出错原因分析

    资源共享问题分析.png
    资源共享问题解决.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<UITouch *> *)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);
    }
    

    测试结果

    num08.png

    互斥锁和自旋锁对比

    共同点

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

    不同点

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

    开发建议

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

    异步下载网络图片

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

    ATS

    <key>NSAppTransportSecurity</key>
    <dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    </dict>
    

    代码实现

    定义属性

    @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?
    num09.png

    相关文章

      网友评论

      • iSeeyer:strong和weak补充还没写完吧?:cry:

      本文标题:NSThread

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