iOS多线程NSThread篇

1,994 阅读12分钟

本篇涉及内容

  • NSThread创建方式
  • NSThread常用属性与API
  • 类方法
  • 实例方法
  • NSObject扩展
  • 属性
  • 相关通知
  • 锁(互斥锁、自旋锁、读写锁、条件锁)的基础释义
  • 锁(互斥锁、自旋锁、读写锁、条件锁)与NSThread的配合使用

NSThread

  • 基于thread封装,添加面向对象概念,性能较差,偏向底层
  • 相对于GCD和NSOperation来说是较轻量级的线程开发
  • 使用比较简单,但是需要手动管理创建线程的生命周期、同步、异步、加锁等问题

本篇文章将介绍一些NSThread的常规使用方法,对底层实现有兴趣的同学可以自行google

一. 属性与API

NSThread创建API

NSThread目前有四种方法,在iOS10以前只有以下两种:

// iOS10以前的类方法
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;  
// iOS10以前的实例方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);  

在iOS10的时候苹果新出了两种创建方法:

// iOS10以后出的类方法
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// iOS10以后出的实例方法
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

四种创建方式使用代码如下:

// iOS10之前NSThread两种方法创建线程。如下:
- (void)createFirstThreadBeforeiOS10 {
    //第一种创建方式 实例方法 最多可以传一个参数
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread1 setName:@"fisrt thread"];
    NSLog(@"create first thread");
    //启动线程
    [thread1 start];
    NSLog(@"start first thread");
}

- (void)createSecondThreadBeforeiOS10 {
    // 第二种创建方式 类方法。此方法不会反悔NSThread对象,创建完毕后直接启动线程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

// iOS 10又添加了两个方法创建线程。
// 需要iOS 10 才可以运行下面两个方法。否则会出错。
- (void)createThirdThreadBeforeiOS10 {
    //第三种创建方式 实例方法
    NSThread *thread3 = [[NSThread alloc] initWithBlock:^{
        for(int i=0;i<10;i++){
            NSLog(@"%@,第三种 i=%d",[NSThread currentThread],i);
        }
    }];
    thread3.name = @"triple kill";
    //调用start方法启动线程
    [thread3 start];
}

- (void)createFourthThreadBeforeiOS10 {
    //第四种创建方式 类方法 此方法也不会返回NSThread对象,直接启动线程
    [NSThread detachNewThreadWithBlock:^{
        for(int i=0;i<10;i++){
            NSLog(@"%@,第四种 i=%d",[NSThread currentThread],i);
        }
    }];
}

线程创建完毕时并不会立即执行,使用类方法创建或者调用- (void)start;方法只是将线程加入可调度线程池,至于什么时候执行需要等待CPU的调度。

NSThread其他API,创建方法上边已经说过,下述所有将跳过创建方法:

以下是NSThread的类方法

  • 获得主线程
+ (NSThread *)mainThread;
使用示例:
NSThread *myMainThread=[NSThread mainThread];
  • 判断当前线程是否是主线程
+ (BOOL)isMainThread;
使用示例:
BOOL isMain = [NSThread isMainThread];
  • 判断当前线程是否是多线程
+ (BOOL)isMultiThreaded;
使用示例:
BOOL isMulti = [NSThread isMultiThreaded];
  • 当前线程休眠到指定日期
+ (void)sleepUntilDate:(NSDate *)date;
使用示例:
NSDate *myDate = [NSDate dateWithTimeInterval:5 sinceDate:[NSDate date]];
[NSThread sleepUntilDate:myDate];
  • 当前线程休眠指定时常
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
使用示例:
[NSThread sleepForTimeInterval:5];
  • 强行退出当前线程
+ (void)exit;
使用示例:
[NSThread exit];
  • 获取当前线程线程优先级
+ (double)threadPriority;
使用示例:
double dPriority=[NSThread threadPriority];
  • 给当前线程设定优先级,调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高。
+ (BOOL)setThreadPriority:(double)p;
使用示例:
BOOL isSetting=[NSThread setThreadPriority:(0.0~1.0)];
  • 线程的调用都会有函数的调用,函数的调用就会有栈返回地址的记录,在这里返回的是函数调用返回的虚拟地址,说白了就是在该线程中函数调用的虚拟地址的数组
+ (NSArray *)callStackReturnAddresses;
使用示例:
NSArray * addressArray = [NSThread callStackReturnAddresses];
  • 同上面的方法一样,只不过返回的是该线程调用函数的名字数字
+ (NSArray *)callStackSymbols;
使用示例:
NSArray * nameNumArray = [NSThread callStackSymbols];

注意:callStackReturnAddresscallStackSymbols这两个函数可以同NSLog联合使用来跟踪线程的函数调用情况,是编程调试的重要手段

以下是NSThread的实例方法

  • 判断是否为主线程
- (BOOL)isMainThread; // 是否为主线程 
使用示例:
BOOL isMain=[tempThread isMainThread];
  • 设置线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
使用示例:
tempThread.name = @"测试线程名称";
[tempThread setName=@"测试线程名称"];
  • 取消线程
- (void)cancel ;
使用示例:
[tempThread cancel];
  • 将线程加入 可调度线程池
- (void)start ;
使用示例:
[tempThread start];
  • 线程初始函数 执行start方法后会自动调用main方法。 main是默认的初始化和调用selector的方法。如果要继承NSThread,可以重写main方法来执行新线程的主要部分。重写的mian方法不需要调用super。不要直接调用mian方法,而是通过start方法来调用。
- (void)main ;
使用示例:[tempThread main];
  • 判断线程是否正在执行
- (void)isExecuting;
使用示例:
BOOL isRunning = [tempThread isExecuting];
  • 判断线程是否已经结束
- (void)isFinished;
使用示例:
BOOL isEnd=[exampleThread isFinished];
  • 判断线程是否撤销
- (void)isCancelled;
使用示例:
isCancel = [tempThread isCancelled];

接下来是NSThread针对NSObject的扩展

  • 在主线程执行方法
// 在主线程上执行函数,wait表示是否阻塞该方法,等待主线程空闲再运,modes表示运行模式kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
使用示例:
[self performSelectorOnMainThread:@Selector(要运行的函数) withObject:@"想要传入的参数" waitUntilDone:YES/NO modes:array];
  • 在主线程执行方法
// 作用与上一下函数相同,但是无法设置运行模式
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
使用示例:
[selfperformSelectorOnMainThread:@Selector(要运行的函数) withObject:@"想要传入的参数" waitUntilDone:(BOOL)wait];
  • 在指定线程执行方法
// 在指定线程上执行函数,wait表示是否阻塞该方法,等待指定线程空闲再运行,modes表示运行模式
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array 
使用示例:
[self performSelector:@Selector(要运行的函数) onThread:(自己指定的线程)withObject:@"想要传入的参数" waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array];
  • 在指定线程执行方法
// 作用与上一下函数相同,但是无法设置运行模式
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
使用示例:
[self performSelector:@Selector(要运行的函数) onThread:(自己指定的线程)withObject:@"想要传入的参数" waitUntilDone:(BOOL)wait];
  • 隐式创建一个线程并执行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
使用示例:
[self performSelectorInBackground:@Selector(要运行的函数) withObject:@"想要传入的参数"];

NSthread的属性

@property (readonly, retain) NSMutableDictionary *threadDictionary;//线程字典
@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;//线程是否可以取消
@property double threadPriority ; //优先级
@property NSQualityOfService qualityOfService ; // 线程优先级
          NSQualityOfServiceUserInteractive:   // 最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
          NSQualityOfServiceUserInitiated:     // 次高优先级,主要用于执行需要立即返回的任务
          NSQualityOfServiceDefault:           // 默认优先级,当没有设置优先级的时候,线程默认优先级
          NSQualityOfServiceUtility:           // 普通优先级,主要用于不需要立即返回的任务
          NSQualityOfServiceBackground:        // 后台优先级,用于完全不紧急的任务

NSThread相关的通知

NSWillBecomeMultiThreadedNotification:由当前线程派生出第一个其他线程时发送,一般一个线程只发送一次
NSDidBecomeSingleThreadedNotification:这个通知目前没有实际意义,可以忽略
NSThreadWillExitNotification线程退出之前发送这个通知

以上,是NSThread的基础属于与API释义与使用

二. 加锁与性能

1. NSLock
// 尝试加锁,成功返回YES ;失败返回NO ,但不会阻塞线程的运行
- (BOOL)tryLock;
// 在指定的时间以前得到锁。YES:在指定时间之前获得了锁;NO:在指定时间之前没有获得锁。该线程将被阻塞,直到获得了锁,或者指定时间过期。
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 返回锁指定的name,有set和get方法
@property (nullable, copy) NSString *name;

下边是一个抢票的demo,涉及到加锁,同步,线程间通讯,延时,解锁,标记,退出等 属性定义

@property (nonatomic, assign) NSInteger ticketNumber;
@property (nonatomic, assign) NSInteger useTicket;
@property (nonatomic, strong) NSThread *ticketThreadOne;
@property (nonatomic, strong) NSThread *ticketThreadTwo;
@property (nonatomic, strong) NSThread *ticketThreadThree;
@property (nonatomic, strong) NSThread *ticketThreadFourth;
@property (nonatomic, strong) NSLock *ticketLock;

初始化

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _ticketNumber = 10;
    _useTicket = 0;
    
    _ticketLock = [[NSLock alloc]init];
    
    _ticketThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadOne.name = @"first thread";
    [_ticketThreadOne start];
    
    _ticketThreadTwo = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadTwo.name = @"second thread";
    [_ticketThreadTwo start];
    
    _ticketThreadThree = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadThree.name = @"third thread";
    [_ticketThreadThree start];
    
    _ticketThreadFourth = [[NSThread alloc] initWithTarget:self selector:@selector(stealTicket)
                                                   object:nil];
    _ticketThreadFourth.name = @"foruth thread";
    [_ticketThreadFourth start];
}

实例方法

- (void)grabTicket {
    while (true) {
        NSThread *currentThread = [NSThread currentThread];
        [_ticketLock lock];
        [NSThread sleepForTimeInterval:0.5];
        NSInteger surplusTicket = _ticketNumber - _useTicket;
        
        if (surplusTicket > 0) {
            _useTicket ++;
            surplusTicket = _ticketNumber - _useTicket;
            NSLog(@"%@成功抢到一张票,剩余票数:%tu",currentThread.name,surplusTicket);
            if ([currentThread.name isEqualToString:@"first thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                [self performSelectorOnMainThread:@selector(EatABun:) withObject:@{@"threadName":@"first thread"} waitUntilDone:YES];
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"吃完馒头继续抢票");
            } else if ([currentThread.name isEqualToString:@"second thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"我是第二条线程,我抢到票之后全体休息3秒钟");
                [NSThread sleepForTimeInterval:3];
            } else if ([currentThread.name isEqualToString:@"third thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"我是第三条线程,我要连抢两张票");
                if (surplusTicket > 0) {
                    _useTicket ++;
                } else {
                    NSLog(@"只剩一张了,没抢上两张");
                }
            }
        } else {
            if ([currentThread isCancelled]) {
                [_ticketLock unlock];
                break;
            } else {
                NSLog(@"%@抢票失败,剩余票数:%tu,所有票都被抢完",[NSThread currentThread],surplusTicket);
                [_ticketThreadOne cancel];
                [_ticketThreadTwo cancel];
                [_ticketThreadThree cancel];
            }
        }
        [_ticketLock unlock];
    }
}

- (void)stealTicket {
    while (true) {
        if ([_ticketThreadFourth isCancelled]) {
            [_ticketLock unlock];
            break;
        }
        NSInteger surplusTicket = _ticketNumber - _useTicket;
        if (surplusTicket <= 0) {
            if ([_ticketLock tryLock]) {
                NSLog(@"我是第四条线程,我过来增加10张票");
                _useTicket = 10;
                [_ticketThreadFourth cancel];
            }
        }
    }
}

- (void)EatABun:(NSDictionary *)threadData {
    NSLog(@"我是第一条线程,抢完票我来吃了一个馒头,%@",[threadData objectForKey:@"threadName"]);
}

需要注意的是:因为线程定义的时候是全局变量,所以线程结束的时候也不会释放。手动调用- (void)cancel;方法后线程将进入死亡状态,在这个状态下再次调用start方法会造成崩溃。

2. NSConditionLock

这个锁的基本释义和使用已经在Pthread篇解释过了,这里就不重复解释了,下边就是演示一下NSConditionLock与NSTHread的配合使用:


@interface NSThreadNSConditionLockController ()

@property (nonatomic, assign) NSInteger useTicket;
@property (nonatomic, strong) NSThread *ticketThreadOne;
@property (nonatomic, strong) NSConditionLock *conditionLock;

@end

@implementation NSThreadNSConditionLockController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = @"NSThreadNSConditionLockController";
    
    _useTicket = 0;
    
    _conditionLock = [[NSConditionLock alloc]initWithCondition:0];
    
    _ticketThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadOne.name = @"first thread";
    [_ticketThreadOne start];
    
}

- (void)grabTicket {
    NSThread *currentThread = [NSThread currentThread];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(true)
        {
            [_conditionLock lockWhenCondition:0];
            // 增加票数
            _useTicket ++;
            NSLog(@"最新票数 = %tu",_useTicket);
            [_conditionLock unlockWithCondition:(_useTicket >= 10 ? 10 : 0)];
  // 打开后执行一次
//            if (_useTicket >= 10) {
//                break;
//            }
            [NSThread sleepForTimeInterval:0.5];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (true)
        {
            [_conditionLock lockWhenCondition:10];
            // 抢票
            _useTicket --;
            NSLog(@"%@成功抢到一张票,剩余票数:%tu",currentThread.name,_useTicket);
            if ([currentThread.name isEqualToString:@"first thread"]) {
                [self performSelectorOnMainThread:@selector(EatABun:) withObject:@{@"threadName":@"first thread"} waitUntilDone:YES];
                NSLog(@"吃完馒头继续抢票");
            }
            [_conditionLock unlockWithCondition:(_useTicket<=0 ? 0 : 10)];
            [NSThread sleepForTimeInterval:0.5];
        }
    });
}

- (void)EatABun:(NSDictionary *)threadData {
    NSLog(@"我是第一条线程,抢完票我来吃了一个馒头,%@",[threadData objectForKey:@"threadName"]);
}

使用这个锁需要注意在线程没有获得锁的情况下,会产生阻塞情况

3. NSRecursiveLock

NSRecursiveLock类定义的锁可以在同一线程多次获得,而不会造成死锁。一个递归锁会跟踪它被多少次成功获得了。每次成功的获得该锁都必须平衡调用锁住和解锁的操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

正如它名字所言,这种类型的锁通常被用在一个递归函数里面来防止递归造成阻塞线程。你可以类似的在非递归的情况下使用他来调用函数,这些函数的语义要求它们使用锁。以下是一个简单递归函数,它在递归中获取锁。如果你不在该代码里使用NSRecursiveLock对象,当函数被再次调用的时候线程将会出现死锁。

NSRecursiveLock除了实现NSLocking协议的方法外,还提供了两个方法,分别如下:

// 在给定的时间之前去尝试请求一个锁
- (BOOL)lockBeforeDate:(NSDate *)limit
// 尝试去请求一个锁,并会立即返回一个布尔值,表示尝试是否成功
- (BOOL)tryLock

下边是在网上找到的一个递归例子:

NSLock *lock = [[NSLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            [lock lock];
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(2);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        
        RecursiveMethod(5);
    });

block里边会重复的调用自己,但是解锁操作在调用之前,这样就导致了死锁,线程被阻塞住了。所以会报这个错误:

-[NSLock lock]: deadlock (<NSLock: 0x6180002c0ee0> '(null)')
Break on _NSLockError() to debug.

这样的情况我们就可以使用NSRecursiveLock,上边例子稍微做一下修改:

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    [NSThread detachNewThreadWithBlock:^{
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            [lock lock];
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        
        RecursiveMethod(5);
    }];

这样就会有完整的输出了

value = 5
value = 4
value = 3
value = 2
value = 1

注意:递归锁不会被主动释放,直到所有锁平衡使用了解锁操作,所以你必须仔细权衡是否使用递归锁,因为它对性能有潜在的影响。长时间持有一个锁将会导致其他线程阻塞直到递归完成。如果你可以重写你的代码来消除递归或消除使用一个递归锁,你可能会获得更好的性能。

3. @synchronized

最后说一下@synchronized这个玩意,@synchronized 结构所表现出来的功能和互斥锁差不多,都是为了防止同一时间不同对象执行同一段代码。 我在这里找到一篇对@synchronized解释的很好的文章,有兴趣的可以自己去看一下,因为这次写的是多线程调研,关于锁这方面的知识就不做深入研究了。 需要了解的:

  • @synchronized 结构在工作时为传入的对象分配了一个递归锁。
  • 你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。
  • 如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。
  • 不要向你的 sychronized block 传入 nil!这将会从代码中移走线程安全。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。

由于@synchronized使用较多,就不做demo解释了



有志者、事竟成,破釜沉舟,百二秦关终属楚;

苦心人、天不负,卧薪尝胆,三千越甲可吞吴.