美文网首页工作生活
深入浅出iOS多线程(二)——pthraed和NSThread的

深入浅出iOS多线程(二)——pthraed和NSThread的

作者: struggle3g | 来源:发表于2019-07-02 04:54 被阅读0次

    深入浅出iOS多线程(一)——线程的概念
    深入浅出iOS多线程(二)——pthraed和NSThread的使用
    深入浅出iOS多线程(三)——GCD多线程
    深入浅出iOS多线程(四)——NSOperation多线程
    深入浅出iOS多线程(五)——多线程锁

    pthread

    pthread简介

    pthread 是属于 POSIX 多线程开发框架,POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),如果想学习这套API,在网上是可以找到相关的资料等,由于在iOS中有NSThrad,如果不考虑移植性,那么在iOS开发中基本上不回去使用,所以只是了解,pthread是多线程的一种技术实现。

    iOS中的pthread

    在iOS中需要导入头文件pthread.h才能够使用pthrad的Api

    #import <pthread.h>
    

    pthraed的特点

    • 一套通用的多线程API
    • 跨平台可移植
    • 使用难度比较大
    • 基于C语言的开发

    pthread的简单使用

    /**
     参数:
     1.指向线程标示的指针
     2.线程的属性
     3.指向函数的指针
     4.传递给该函数的参数
     
     返回值
     - 如果是0,标示正确
     - 如果非0,标示错误代码
     
     void *   (*)      (void *)
     返回值   (函数指针)  (参数)
     void *  和OC中的  id 是等价的!
     
     */
        
    pthread_t pthreadId ;
        
    NSString *str = @"敲代码";
        
    int result = pthread_create(&pthreadId, 
                                      NULL, 
                                    &doing, 
                     (__bridge void *)(str)
                               );
        
    if(result == 0){
        NSLog(@"开启成功");
    }else{
        NSLog(@"开启失败");
    }
    
    
    void * doing(void * param){
        
        NSLog(@"%@,%@",[NSThread currentThread],param);
        return NULL;
    }
    
    

    NSThread

    iOS的多线程NSThread简介

    NSThread是苹果官方提供面向对象操作线程的技术,简单方便,可以直接操作对象,需要手动控制线程的生命周期,平时iOS开发较少使用,使用最多的是获取当前线程

    NSThread特点

    • 面向对象的多线程编程
    • 简单易用,可直接操作线程对象
    • 需要手动管理线程的生命周期

    NSThread的详细使用介绍

    如何开启NSThread线程

    NSThread初始化API

    //初始化的API
    - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument 
    - (instancetype)initWithBlock:(void (^)(void))block 
    
    //类对象方法
    + (void)detachNewThreadWithBlock:(void (^)(void))block 
    + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
    

    上述实例方法以及类对象方法,一样都是创建一个新的线程,不一样的是,类对象方法不需要创建完成以后调用start方法,而alloc创建的线程需要手动start开启。

    NSThread代码实现

    //方法一:
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(demo1Doing:) object:@"hello"];
    [thread start];
        
    //方法二:
    NSThread *thread1 = [[NSThread alloc]initWithBlock:^{
        NSLog(@"%s",__func__);
    }];
    [thread1 start];
        
    //方法三:
    [NSThread detachNewThreadSelector:@selector(demo1Doing:) toTarget:self withObject:@"hello"];
        
    //方法四
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"%s",__func__);
    }];
    

    NSThread主线程的API和获取主线程

    • 判断是否是主线程,获取主线程

      + (NSThread *)mainThread; // 获得主线程
      - (BOOL)isMainThread; // 是否为主线程
      + (BOOL)isMainThread; // 是否为主线程
      
    • 获取当前线程

      NSThread *current = [NSThread currentThread];
      
      
    • 设置和获取线程的名字

      - (void)setName:(NSString *)n;
      - (NSString *)name;
      

    其他创建线程的方式

    • 自启动线程

      [NSThread detachNewThreadSelector:@selector(demo1Doing:) toTarget:self withObject:@"hello"];
          
      [NSThread detachNewThreadWithBlock:^{
          NSLog(@"%s",__func__);
      }];
      
    • 隐式创建并启动线程

      - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
      - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
      - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
      - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
      - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
      

    总结

    上述两种创建线程方式的优缺点:

    • 优点:简单快捷
    • 缺点:无法对线程进行更详细的设置和管理

    控制线程状态

    • 启动线程

      // 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
      - (void)start; 
      
    • 阻塞线程

      //进入阻塞线程状态  休眠
      + (void)sleepUntilDate:(NSDate *)date;
      + (void)sleepForTimeInterval:(NSTimeInterval)ti;
      
    • 强制停止线程

      //进入死亡状态
      + (void)exit;
      

    线程的优先级

    • 线程的优先级API

      + (double)threadPriority;
      + (BOOL)setThreadPriority:(double)p;
      
    • 优先级设置

      • Priorit的值 0.0~1.0之间
      • 优先级只能保证CPU调度的可能性会高,归根究底你还是无法控制多线程的顺序,如果就靠优先级,来误认为控制多线程额顺序是不严谨的。
        • 多线程的目的:是不阻塞UI线程
        • 建议不要修改优先级
        • 多线程开发中不能相信一次的运行结果
        • 优先级翻转,优先级低的任务太耗时放到最后面,然后后面排的任务比较多,优先级高的任务被堵死了

    多线程的安全隐患问题

    安全隐患?

    • 资源共享
      • 一块资源可能会被多个线程共享,也就是说多个线程可能会访问同一块资源
      • 例如:多个线程同时访问修改同一个变量、同一个文件、同一个对象
    • 数据错乱
      • 同一个线程修改一个数据,不同线程不同的运行结束时间,有可能得到不一样结果

    安全隐患问题分析

    • ThreadA去访问一块内容中的integer数据,得到数据17,17+1 = 18写入内存
    • ThreadB也在同一时间访问了integer数据,得到数据17,17+1 = 18写入内存
    • 结果非常有意思的是+1了两次应该是19才对,最终结果是18,这就是多线程的安全隐患问题,如下图所示
      安全隐患.png

    如何解决多线程的安全隐患(线程锁)

    互斥锁

    • 当ThreadA去访问一块内容中的integer数据的时候,首先上一把锁lock得到数据17,17+1 = 18写入内存,最后在unlock

    • ThreadB也在同一时间访问了integer数据:

      • 由于ThreadA已经在数据上面加了锁lock,所以必须等到ThreadA完成以后才能去访问这个数据

      • ThreadA完成,访问integer数据时,时候lock然后在获取数据18,18+1 = 19写入内存,最后在unlock

        //互斥锁
        @synchronized (self) {
        }
        ``  
        
        
      • @synchronized的参数:

        • 任意OC对象都可以加锁
        • 加锁一定要加锁共有的对象,一般用self
    • 互斥锁的优缺点:

      • 优点:能有效防止因多线程抢夺资源造成的数据安全问题
      • 缺点:需要消耗大量的CPU资源
    • 互斥锁的使用前提

      • 多条线程抢夺同一块资源
    • 线程同步

      • 线程同步的意思就是:多条线程同一条线上工作(按顺序地执行任务)
      • 互斥锁,就使用了线程同步技术

    互斥锁需要注意的地方

    • 保证代码内的代码,同一时间,只有一条线程执行

    • 互斥锁的范围应该尽量小,范围大了,效率就差

    • 结果本身是一个多线程开发,最后结果变成了同步去执行,互斥锁,也就是同步线程技术,如下图所示:

      互斥锁.png

    如何解决多线程的安全隐患(原子与非原子对象)

    nonatomic 非原子属性

    • 不会为setter方法加锁
    • 非原子属性,因为atomic所有对这个对象的操作之前会加锁,所以会很耗费资源,在没有安全隐患的问题上在加锁,是不必要的

    atomic 原子属性

    • 为setter方法加锁(默认是atomic)

    • 原子属性,保证这个属性的安全性(线程安全),多线程写入这个对象的时候,保证同一时间只有一个线程能够执行!

      • 模拟一个atomic原子属性
      //模拟原子属性
      - (void)setMyAtomic:(NSObject *)myAtomic{
          @synchronized (self) {
              _myAtomic = myAtomic;
          }
      }
      
      • 实际上,原子属性内部有一个锁,自旋锁:
      • 自旋锁和互斥锁不一样的地方
        • 共同点:都能够保证线程的安全
        • 不同点:互斥锁:如果线程被锁到外面,线程就会进入休眠状态,等待锁打开,打开之后被唤醒;自旋锁:如果线程被锁在外面,就会用死循环的方式,一直等待锁打开。
      • 无论什么锁,都会消耗新能,效率不高
      • 线程安全
      • 在多个线程进行读写操作时,仍然保证数据正确
    • UI线程

      • 共同的约定,所有更新UI的操作都放在主线程执行
      • 因为UIKit 框架都是线程不安全的(因为线程安全效率低下)
    • nonatomic和atomic对比

      • atomic:线程安全,需要消耗大量的资源
      • nonatomic:非线程安全,适合内存小的移动设备
    • iOS开发使用nonatomic和atomic

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

    注意一个小细节

    • OC中:定义一个属性,通常会生成成员变量,如果同时重写了getter、setter那么成员变量就不会自动生成
      • 如果想要同时重写了getter、setter,那么就直接使用 @synthesize myAtomic = _myAtomic;

    NSThread自定义

    在NSThread的有init初始化方法:

    //用alloc init 适用于自定义NSThread (子类)
    NSThread * t = [[NSThread alloc]init];
    需要创建一个新的子类继承NSThread方法,然后重写main方法
    

    多线程下载网络图片

    -(void)loadView{
    }
    

    如果重新了上述方法,SB和XIB都无效

    代码如下:

    #import "ViewController.h"
    
    @interface ViewController ()<UIScrollViewDelegate>
    @property(nonatomic,strong)UIScrollView * scrollView;
    @property(nonatomic,weak) UIImageView * imageView;
    @property(nonatomic,strong) UIImage * image;
    @end
    
    @implementation ViewController
    
    
    /**
     加载视图结构的,纯代码开发
     功能 SB&XIB 是一样
     如果重写了这个方法,SB和XIB 都无效
     */
    -(void)loadView{
        //搭建界面
        self.scrollView = [[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].bounds];
        self.view = self.scrollView;
        //MARK:- 设置缩放属性
        self.scrollView.delegate = self;
        self.scrollView.minimumZoomScale = 0.1;
        self.scrollView.maximumZoomScale = 2.0;
        
        
        //imageView
        UIImageView * iv = [[UIImageView alloc]init];
        //会调用View的getter方法. loadView方法在执行的过程中!如果self.view == nil,会自动调用loadView加载!
        [self.view addSubview:iv];
        self.imageView = iv;
    }
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    
        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage) object:nil];
        [t1 start];
        
    
    }
    
    //MARK: - 下载图片
    -(void)downloadImage{
        
        NSLog(@"%@",[NSThread currentThread]);
        
        //NSURL -> 统一资源定位符,每一个URL 对应一个网络资源!
        NSURL * url = [NSURL URLWithString:@"https://images.unsplash.com/photo-1496840220025-4cbde0b9df65?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80"];
        
        //下载图片(在网络上传输的所有数据都是二进制!!)
        //为什么是二进制:因为物理层!!是网线!!网线里面是电流!!电流有高低电频!!高低电频表示二进制!!!
        NSData * data = [NSData dataWithContentsOfURL:url];
        
        //将二进制数据转成图片并且设置图片
        //提示:不是所有的更新UI在后台线程支持都会有问题!!!
        //重点提示:不要去尝试在后台线程更新UI!!!出了问题是非常诡异的!!
        //    self.image = [UIImage imageWithData:data];
        
        //在UI线程去更新UI
        /**
         * 1.SEL:在主线程执行的方法
         * 2.传递给方法的参数
         * 3.让当前线程等待 (注意点!! 如果当前线程是主线程!哥么YES没有用!!)
         */
        // 线程间通讯
        [self performSelectorOnMainThread:@selector(setImage:) withObject:[UIImage imageWithData:data] waitUntilDone:NO];
        
        
        
    }
    
    
    //这种写法 省略一个 _image ,主要原因是因为image 保存在了imageView里面了!
    -(UIImage *)image{
        return self.imageView.image;
    }
    
    
    -(void)setImage:(UIImage *)image{
        NSLog(@"更新 UI 在====%@",[NSThread currentThread]);
        //直接将图片设置到控件上
        self.imageView.image = image;
        //让imageView和image一样大
        [self.imageView sizeToFit];
        //指定ScrollView 的contentSize
        self.scrollView.contentSize = image.size;
        
        NSLog(@"\n\n\n\n\n\n\n\n\n\n\n%@",self.image);
    }
    
    #pragma mark - <scrollView代理>
    //告诉 ScrollView 缩放哪个View
    -(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
        return self.imageView;
    }
    
    /**
     * transform 矩阵
     *  CGFloat a(缩放比例), b, c, d(缩放比例);  共同决定角度!
     *  CGFloat tx(x方向位移), ty(y方向的位移);
     
     *
     */
    -(void)scrollViewDidZoom:(UIScrollView *)scrollView
    {
        NSLog(@"%@",NSStringFromCGAffineTransform(self.imageView.transform));
    }
    
    @end
    
    
    • 提示:不是所有的更新UI在后台线程支持都会有问题!!!

    • 重点提示:不要去尝试在后台线程更新UI!!!出了问题是非常诡异的!!

    • 在UI线程去更新UI

       /**
       * 1.SEL:在主线程执行的方法
       * 2.传递给方法的参数
       * 3.是否让当前线程等待 (注意点!! 如果当前线程是主线程!YES没有用!!)
       * NO当前线程不需要等待@selector(setImage:)执行完成,YES当前线程需要等待@selector(setImage:)执行完成
       */
       // 线程间通讯
      [self performSelectorOnMainThread:@selector(setImage:) withObject:[UIImage imageWithData:data] waitUntilDone:NO];
      

    线程间的通信

    @interface NSObject (NSThreadPerformAdditions)
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    @end
    

    NSPort实现线程通信

    代码如下:

    @interface ViewController () <NSPortDelegate>
    @property (nonatomic, strong) NSPort* subThreadPort;
    @property (nonatomic, strong) NSPort* mainThreadPort;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.mainThreadPort = [NSPort port];
        self.mainThreadPort.delegate = self;
        [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
        [self task];
    }
    
    - (void) task {
        NSThread* thread = [[NSThread alloc] initWithBlock:^{
            self.subThreadPort = [NSPort port];
            self.subThreadPort.delegate = self;
            
            [[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        }];
        [thread setName:@"子线程"];
        [thread start];
    }
    
    - (void)handlePortMessage:(id)message {
        NSLog(@"%@", [NSThread currentThread]);
        
        if (![[NSThread currentThread] isMainThread]) {
            NSMutableArray* sendComponents = [NSMutableArray array];
            NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
            [sendComponents addObject:data];
            [self.mainThreadPort sendBeforeDate:[NSDate date] components:sendComponents from:self.subThreadPort reserved:0];
            return;
        }
        sleep(2);
        NSMutableArray* components = [message valueForKey:@"components"];
        
        if ([components count] > 0) {
            NSData* data = [components objectAtIndex:0];
            NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(@"%@", str);
        }
    
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        NSMutableArray* components = [NSMutableArray array];
        NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];
        
        [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
    }
    
    @end
    

    NSThread需要注意的地方

    子线程执行太快,还会调用线程通信的代码吗?

    不会
    有时候会出现这个问题,代码如下:

        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
        [t1 start];
        //不执行地方原因,是因为 demo 方法执行的快!""
        [self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
        
    -(void)demo{
        NSLog(@"%@",[NSThread currentThread]);
    }
    -(void)otherMethod{
    
        self.finished = YES;
    }
    
    • Demo执行额太快,因为子线程是没有RunLoop的,当demo执行完成以后就消失了,所以不会在执行otherMethod

    如何解决上述问题

    在子线程开启RunLoop,

        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
        [t1 start];
        //不执行地方原因,是因为 demo 方法执行的快!""
        [self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
        
    -(void)demo{
        NSLog(@"%@",[NSThread currentThread]);
       [[NSRunLoop currentRunLoop] run];
    }
    -(void)otherMethod{
        NSLog(@"%s %@",__FUNCTION__,[NSThread currentThread]);
    }
    
    • RunLoop开启了循环,这样就会无限制的进行循环,这样这个子线程就永远不会释放

    • 改进的办法就是,从外面创建一个BOOL来判断是否需要关闭RunLoop

    @interface ViewController ()
    /** 循环条件 */
    @property(assign,nonatomic,getter=isFinished)BOOL finished;
    @end
    
    @implementation ViewController
    
    
        NSThread * t1 = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
        [t1 start];
        
        self.finished = NO;
        
        //不执行地方原因,是因为 demo 方法执行的快!""
        [self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
    
    -(void)demo{
        NSLog(@"%@",[NSThread currentThread]);
        //启动当前RunLoop  哥么就是一个死循环!!
        //使用这种方式,可以自己创建一个线程池!
        //    [[NSRunLoop currentRunLoop] run];
        
        //在OC中使用比较多的,退出循环的方式!
        while (!self.isFinished) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        }
    }
    -(void)otherMethod{
        for (int i = 0; i<10; i++) {
            
            NSLog(@"%s %@",__FUNCTION__,[NSThread currentThread]);
        }
        self.finished = YES;
    }
    
    

    相关文章

      网友评论

        本文标题:深入浅出iOS多线程(二)——pthraed和NSThread的

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