iOS 多线程(三)NSThread的使用

一、NSThread基本介绍

  NSThread是OC中封装程度最小最轻量级的,使用更灵活,基本使用比较简单,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大,在平时使用很少,最常用到的无非就是[NSThread currentThread]获取当前线程。
  NSObject基类对象提供有隐式快速创建NSThread线程的performSelector系列类别扩展工具方法。NSThread还提供了一些静态工具接口来控制当前线程以及获取当前线程的一些信息。

二、NSThread初始化和属性

1、初始化
//创建线程
NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(demo:) object:@"Thread"];
//或者
NSThread  *newThread=[[NSThread alloc]init];
NSThread  *newThread= [[NSThread alloc]initWithBlock:^{
       NSLog(@"initWithBlock");
}];
2、属性
@property (class, readonly, strong) NSThread *currentThread;//当前线程
/**
每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。
你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
比如,你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代的状态信息。
NSThread实例可以使用一下方法
*/
@property (readonly, retain) NSMutableDictionary *threadDictionary;
NSMutableDictionary *dict = [thread threadDictionary]; 

//优先级
@property double threadPriority ;

/** NSQualityOfService:
  NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
  NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回的任务
  NSQualityOfServiceDefault:默认优先级,当没有设置优先级的时候,线程默认优先级
  NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回的任务
  NSQualityOfServiceBackground:后台优先级,用于完全不紧急的任务
*/
@property NSQualityOfService qualityOfService; 
@property (nullable, copy) NSString *name;//线程名称 
@property NSUInteger stackSize ;//线程使用栈区大小,默认是512K 
@property (readonly, getter=isExecuting) BOOL executing;//线程是否在执行 
@property (readonly, getter=isFinished) BOOL finished;//线程是否执行结束 
@property (readonly, getter=isCancelled) BOOL cancelled;//线程是否取消

三、实例方法

- (void)start;//启动线程
- (BOOL)isMainThread;//是否为主线程
- (void)setName:(NSString *)n;//设置线程名称
- (void)cancel ;//取消线程
- (void)main ;//线程的入口函数
- (void)isExecuting;//判断线程是否正在执行
- (void)isFinished;//判断线程是否已经完成
- (void)isCancelled; //判断线程是否取消

四、类方法

+ (void)detachNewThreadWithBlock:(void (^)(void))block;//block方式
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;//SEL方式
 
+ (void)currentThread;//获取当前线程 
+ (BOOL)isMultiThreaded;//当前代码运行所在线程是否是子线程
+ (void)sleepUntilDate:(NSDate *)date;//当前代码所在线程睡到指定时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti; //当前线程睡多长时间
+ (void)exit;//退出当前线程
+ (double)threadPriority;//设置当前线程优先级

//给当前线程设定优先级,调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高。
+ (BOOL)setThreadPriority:(double)p;

//线程的调用都会有函数的调用函数的调用就会有栈返回地址的记录,在这里返回的是函 数调用返回的虚拟地址,说白了就是在该线程中函数调用的虚拟地址的数组
+ (NSArray *)callStackReturnAddresses;

//同上面的方法一样,只不过返回的是该线程调用函数的名字数字
+ (NSArray *)callStackSymbols;

五、隐式创建&线程间通讯

  以下方法位于NSObject (NSThreadPerformAdditions)分类中,所有继承NSObject 实例化对象都可调用以下方法

/**
  指定方法在主线程中执行
参数:
    1. SEL 方法
    2.方法参数
    3.是否等待当前执行完毕
    4.指定的Runloop model
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    // equivalent to the first method with kCFRunLoopCommonModes
/**
  指定方法在某个线程中执行
参数:
    1. SEL 方法
    2.方法参数
    3.是否等待当前执行完毕
    4.指定的Runloop model
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    // equivalent to the first method with kCFRunLoopCommonModes
/**
  指定方法在开启的子线程中执行
参数:
    1. SEL 方法
    2.方法参数
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

  注意:我们经常提到的线程间通讯其实就是上面几个方法,并不是多高大上,也没有多复杂!!!同时苹果声明UI更新一定要在UI线程(主线程)中执行,虽然不是所有后台线程更新UI都会出错。

六、线程间资源共享&线程加锁

  在程序运行过程中,如果存在多线程,那么各个线程读写资源就会存在先后、同时读写资源的操作,因为是在不同线程,CPU调度过程中我们无法保证哪个线程会先读写资源,哪个线程后读写资源。因此为了防止数据读写混乱和错误的发生,我们要将线程在读写数据时加锁,这样就能保证操作同一个数据对象的线程只有一个,当这个线程执行完成之后解锁,其他的线程才能操作此数据对象。NSLock / NSConditionLock / NSRecursiveLock / @synchronized都可以实现线程上锁的操作。

1、@synchronized

直接上例子:相信12306卖火车票的例子大家了解
首先:开启两个线程同时售票

    self.tickets = 20;
    NSThread *t1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
    t1.name = @"售票员A";
    [t1 start];
    
    NSThread *t2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
    t2.name = @"售票员B";
    [t2 start];

然后:将售票的方法加锁

- (void)saleTickets{
    while (YES) {
        [NSThread sleepForTimeInterval:1.0];
        //互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
        @synchronized (self) {
            //1.判断是否有票
            if (self.tickets > 0) {
                //2.如果有就卖一张
                self.tickets --;
                NSLog(@"还剩%d张票  %@",self.tickets,[NSThread currentThread]);
            } else {
                //3.没有票了提示
                NSLog(@"卖完了 %@",[NSThread currentThread]);
                break;
            }
        }
    } 
}
2、NSLock
-(BOOL)tryLock;//尝试加锁,成功返回YES ;失败返回NO ,但不会阻塞线程的运行
/** 在指定的时间以前得到锁。
    YES:在指定时间之前获得了锁;
    NO:在指定时间之前没有获得锁。
    该线程将被阻塞,直到获得了锁,或者指定时间过期。
*/
-(BOOL)lockBeforeDate:(NSDate *)limit;
- (void)setName:(NSString*)newName//为锁指定一个Name
- (NSString*)name//**返回锁指定的**name

@property (nullable, copy) NSString *name;线程锁名称 

举个例子:

NSLock* myLock=[[NSLock alloc]init];
NSString *str=@"hello";
[NSThread detachNewThreadWithBlock:^{
            [myLock lock];
            NSLog(@"%@",str);
            str=@"world";
            [myLock unlock];
}];
[NSThread detachNewThreadWithBlock:^{
            [myLock lock];
            NSLog(@"%@",str);
            str=@"变化了";
            [myLock unlock];
}];

输出结果不加锁之前,两个线程输出一样 hello;加锁之后,输出分辨为hello 与world。

3、NSConditionLock

使用此锁,在线程没有获得锁的情况下,阻塞,即暂停运行,典型用于生产者/消费者模型。

- (instancetype)initWithCondition:(NSInteger)condition;//初始化条件锁
- (void)lockWhenCondition:(NSInteger)condition;//加锁 (条件是:锁空闲,即没被占用;条件成立)
- (BOOL)tryLock; //尝试加锁,成功返回TRUE,失败返回FALSE
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//在指定条件成立的情况下尝试加锁,成功返回TRUE,失败返回FALSE
- (void)unlockWithCondition:(NSInteger)condition;//在指定的条件成立时,解锁
- (BOOL)lockBeforeDate:(NSDate *)limit;//在指定时间前加锁,成功返回TRUE,失败返回FALSE,
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//条件成立的情况下,在指定时间前加锁,成功返回TRUE,失败返回FALSE,

@property (readonly) NSInteger condition;//条件锁的条件
@property (nullable, copy) NSString *name;//条件锁的名称

举个例子:

  NSConditionLock* myCondition=[[NSConditionLock alloc]init];
    [NSThread detachNewThreadWithBlock:^{
        for(int i=0;i<5;i++) {
            [myCondition lock];
            NSLog(@"当前解锁条件:%d",i);
            sleep(2);
            [myCondition unlockWithCondition:i];
            BOOL isLocked=[myCondition tryLockWhenCondition:2];
            if (isLocked) {
                NSLog(@"加锁成功!!!!!");
                [myCondition unlock];
            }
        }
    }];

输出结果,在条件2 解锁之后,等待条件2 的锁加锁成功。

4、NSRecursiveLock

此锁可以在同一线程中多次被使用,但要保证加锁与解锁使用平衡,多用于递归函数,防止死锁。

- (BOOL)tryLock;//尝试加锁,成功返回TRUE,失败返回FALSE
- (BOOL)lockBeforeDate:(NSDate *)limit;//在指定时间前尝试加锁,成功返回TRUE,失败返回FALSE
@property (nullable, copy) NSString *name;//线程锁名称

使用示例:

- (void)initRecycle:(int)value {
   [myRecursive lock];
   if (value > 0) {
       NSLog(@"当前的value值:%d",value);
       sleep(2);
       [self initRecycle:value - 1];
   }
   [myRecursive unlock];
}

输出结果: 从你传入的数值一直到1,不会出现死锁

七、线程安全之原子属性 atomic

1、原子属性(线程安全)与非原子属性,平时我们 @property声明对象属性时会用到nonatomic
2、苹果系统在我们声明对象属性时默认是atomic,也就是说在读写这个属性的时候,保证同一时间内只有一个线程能够执行。当声明时用的是atomic,通常会生成 _成员变量,如果同时重写了getter&setter _成员变量 就不自动生成。实际上原子属性内部有一个锁,叫做自旋锁

image.png

@property (strong, nonatomic) NSObject *myNonatomic;
@property (strong,    atomic) NSObject *myAtomic;

根据上面描述,我们得出结论,当我们重写了myAtomic的setter和getter方法

- (void)setMyAtomic:(NSObject *)myAtomic{
      _myAtomic = myAtomic;
}
- (NSObject *)myAtomic{
    return _myAtomic;
}

那么我们就必须声明一个_myAtomic静态变量

@synthesize myAtomic = _myAtomic;

否则系统在编译的时候找不到 _myAtomic

八、子线程上的Runloop

1、Runloop基本介绍(runloop阶段详细讲解):

Runloop:ios运行循环机制
目的:保证程序不退出
监听事件:没有事件让程序进入休眠
区分模式:NSDefaultRunLoopMode - 时钟、网络事件等

void click(int type){
    printf("正在运行第%d",type);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        while (YES) {
            printf("请输入选项 0 表示退出");
            int result = -1;
            scanf("%d",&result);
            if (result == 0) {
                printf("程序结束\n");
                break;
            }else{
                click(result);
            }
        }
    }
    return 0;
}

2、在iOS中,子线程上的Runloop默认不开启的,并且子线程中的Runloop开启之后是手动无法关闭的,同时即使开启之后如果不添加任何任务,Runloop会进入休眠状态,获取不到当前的Runloop

@property (assign, nonatomic, getter=isFinished) BOOL finished;

创建子线程并添加任务
    NSThread *t = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
    [t start];
    self.finished = NO;
    [self performSelector:@selector(otherMethod) onThread:t withObject:nil waitUntilDone:NO];

在第一个任务中加入死循环
- (void)demo{
    NSLog(@"%@",[NSThread currentThread]);
    //在OC中使用比较多的,退出循环的方式
    while (!self.isFinished) {
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
    }
    NSLog(@"能来吗?");
}

在最后添加的任务结束后结束死循环

- (void)otherMethod{
    for (int i = 0; i < 10; i ++) {
        NSLog(@"%s   %@",__FUNCTION__,[NSThread currentThread]);=
    }
  //让上面方法中的死循环结束
   self.finished = YES; 

参考博客:https://www.jianshu.com/p/686dbf4bbb52

你可能感兴趣的:(iOS 多线程(三)NSThread的使用)