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

深入浅出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;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容