iOS NSThread进阶 详

当需要对一个知识详细深入了解的时候,最怕的事情就是,官方文档什么也不说就是光溜溜的代码,就像NSThread,注释官方文档奉上。

@interface NSThread : NSObject  {
@private
    id _private;
    uint8_t _bytes[44];
}

// 获取当前线程
+ (NSThread *)currentThread;

// 创建新线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 是否是多线程
+ (BOOL)isMultiThreaded;

/** 
*   每个线程都维护了一个“键-值”的字典,它可以在线程里面的任何地方被访问,
*   可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
*   比如,可以使用它来存储在整个线程过程中RunLoop里面多次迭代的状态信息。
*   使用:通过threadDictionary方法获取一个NSMutableDictionary对象,然后添加需要的字段和数据
*/
@property (readonly, retain) NSMutableDictionary *threadDictionary;

// 设置线程睡眠/堵塞
+ (void)sleepUntilDate:(NSDate *)date;

// 设置线程睡眠/堵塞
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 结束/退出进程
+ (void)exit;

// 获取线程的优先级
+ (double)threadPriority;

// 设置线程优先级,取值范围0.0~1.0
+ (BOOL)setThreadPriority:(double)p;

// 线程优先级,iOS8以后推荐使用qualityOfService属性,通过量化的优先级枚举值来设置
@property double threadPriority; 

/** 线程优先级
    qualityOfService的枚举值如下:
    NSQualityOfServiceUserInteractive:最高优先级,用于用户交互事件
    NSQualityOfServiceUserInitiated:次高优先级,用于用户需要马上执行的事件
    NSQualityOfServiceDefault:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
    NSQualityOfServiceUtility:普通优先级,用于普通任务
    NSQualityOfServiceBackground:最低优先级,用于不重要的任务
*/
@property NSQualityOfService qualityOfService;

// 返回当前线程在栈中所占的地址所组成的数组
+ (NSArray *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);

// 返回栈空间的符号表
+ (NSArray *)callStackSymbols NS_AVAILABLE(10_6, 4_0);

// 线程名称
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

// 栈的所占空间大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);

// 是否是主线程
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);

// 判断当前线程是否是主线程
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main

// 获取主线程
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

// 初始化线程
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;

// 初始化线程
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);

// 是否正在执行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);

// 是否执行完毕
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);

// 是否已经取消/中止
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

// 取消线程,不能再开始
- (void)cancel NS_AVAILABLE(10_5, 2_0);

// 开始线程
- (void)start NS_AVAILABLE(10_5, 2_0);

/** main是线程入口
*  - (void)main的使用:
*  1. 一般创建线程会子类化NSThread,重写main方法,把关于线程执行的方法都写在里面,这样可以在任何需要这个线程方法的地方直接使用。
*  2. 把线程执行的方法写在main里,是因为线程的操作应该属于线程的本身,而不是每次使用都通过initWithTarget:selector:object:方法,且再一次实现某个方法。
*  3. 当重写了main方法后,同时使用initWithTarget:selector:object:方法初始化,调用某个方法执行任务,系统默认只执行main方法里面的任务。
*  4. 如果直接使用NSThread创建线程,线程内执行的方法都是在当前的类文件里面的。
*/
- (void)main NS_AVAILABLE(10_5, 2_0);

@end


FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;

@interface NSObject (NSThreadPerformAdditions)
/**
将需要执行的任务放到主线程进行操作
* aSelector:方法Id 
* arg:需要传的参数
* wait:指定,当前线程是否要被阻塞,直到主线程将我们制定的代码块(RefreshCellForLiveId:方法)执行完。
注意:
1.当前线程为主线程的时候,waitUntilDone:YES参数无效。
2.该方法,没有返回值
3.该方法主要用来用主线程来修改页面UI的状态。
4.modes:(nullable NSArray *)array 这个参数我不知道从何而来 
*/
- (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
//指定任务在特定的线程上执行
- (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
//指定任务到后台执行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

NSThread线程创建

方式一:

- (void)viewDidLoad {
    [super viewDidLoad];
    //此方式需要调用start方法
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(text_thread:) object:@"init"];
    [thread start];
}
- (void)text_thread:(id)obj{
    NSLog(@"obj:%@\ncurrentThread:%@",obj,[NSThread currentThread]);
}

打印数据
2018-04-26 15:33:28.321263+0700 GCD[79984:4079183] obj: init
currentThread:{number = 3, name = (null)}

方式二

- (void)viewDidLoad {
    [super viewDidLoad];
  //此方式不需要调用start方法
    [NSThread detachNewThreadSelector:@selector(text_thread:) toTarget:self withObject:@"detachNew"];
    
}
- (void)text_thread:(id)obj{
    NSLog(@"obj:%@\ncurrentThread:%@",obj,[NSThread currentThread]);
}

打印数据
2018-04-26 15:41:02.738780+0700 GCD[80110:4086842] obj:detachNew
currentThread:{number = 3, name = (null)}

方式三

- (void)viewDidLoad {
    [super viewDidLoad];
    //perform 创建三种方式
    // 这三个方法都是同步执行,与线程无关,在需要动态的去调用方法的时候去使用
    //[self performSelector:@selector(threadRun)];
    //[self performSelector:@selector(threadRun) withObject:nil];
    //[self performSelector:@selector(threadRun) withObject:nil afterDelay:2.0];
  //此方式不需要调用start方法
    [self performSelectorInBackground:@selector(text_thread:)withObject:@"perform"];
    
}
- (void)text_thread:(id)obj{
    NSLog(@"obj:%@\ncurrentThread:%@",obj,[NSThread currentThread]);
}

打印数据:
2018-04-26 15:46:45.520661+0700 GCD[80178:4091323] obj:perform
currentThread:{number = 3, name = (null)}

NSThread属性设置

- (void)viewDidLoad {
    [super viewDidLoad];
    //此方式不需要调用start方法
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(text_thread:) object:@"线程属性"];
    //设置线程名称
    thread2.name = @"download B";
    /**threadPriority:优先级的取值范围为0.0-1.0,线程默认优先级是0.5,最高是1.0。
    优先级高只能说明 CPU 在调度的时候,会优先调度,并不意味着优先级低的就不被调用或者后调用!
    在多线程开发的时候,不要去做不同线程之间执行的比较!线程内部的方法都是各自独立执行的,如果设置了优先级,那么就会有可能出现低优先级的线程阻塞高优先级的线程,也就是优先级反转!在ios开发中,多线程最主要的目的就是把耗时操作放在后台执行,一半不做修改**/
    thread2.threadPriority = 0;
    //修改当前线程栈区大小 单位KB 默认512KB 一般不做修改
    thread2.stackSize = 11024 * 1024;
    [thread2 start];
    
}
- (void)text_thread:(id)obj{
    NSLog(@"obj:%@\ncurrentThread:%@\n主线程栈区空间大小:%ld",obj,[NSThread currentThread],[NSThread currentThread].stackSize);
}

打印数据:
2018-04-26 16:08:29.396115+0700 GCD[80413:4108213] obj:线程属性
currentThread:{number = 3, name = download B}
主线程栈区空间大小:11288576

阻塞线程

// 设置线程睡眠/堵塞
+ (void)sleepUntilDate:(NSDate *)date;
// 设置线程睡眠/堵塞
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

注意:这两个方法相同
[NSThread sleepForTimeInterval:5.0]也就等于[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]

取消,判断取消 以及退出方法

//开始
- (IBAction)start:(id)sender {
    _thread = [[NSThread alloc]initWithTarget:self selector:@selector(startCount) object:nil];
    [_thread start];
}
//取消线程
- (IBAction)cancel:(id)sender {
    //并没有真正取消该线程,只是给该线程设置了一个标志位
    NSLog(@"取消线程");
    [_thread cancel];
    NSLog(@"_thread:%@",_thread);
}
//执行任务
- (void)startCount{
    for (NSInteger i = 0; i< 1000; i++) {
        //根据线程是否取消的标志位退出该任务 退出该任务后会发现全部不执行了_thread 也没有进行打印
        if (_thread.cancelled) {
            [NSThread exit];
            NSLog(@"退出:_thread:%@",_thread);
            return;
        }
        [NSThread sleepForTimeInterval:1.0f];
        NSLog(@"%ld",i);
    }
} 

打印数据:
2018-04-26 20:47:32.739797+0700 GCDCC[41269:2619697] 0
2018-04-26 20:47:33.743628+0700 GCDCC[41269:2619697] 1
2018-04-26 20:47:34.747350+0700 GCDCC[41269:2619697] 2
2018-04-26 20:47:35.751858+0700 GCDCC[41269:2619697] 3
2018-04-26 20:47:36.394613+0700 GCDCC[41269:2617587] 取消线程
2018-04-26 20:47:36.395097+0700 GCDCC[41269:2617587] _thread:{number = 8, name = main}
2018-04-26 20:47:36.754946+0700 GCDCC[41269:2619697] 4

主线程相关方法

//此处不做解释
+ (NSThread*)mainThread;// 获得主线程  
- (BOOL)isMainThread;// 是否为主线程  
+ (BOOL)isMainThread;// 是否为主线程 

指定任务线程到主线程进行操作

说明:

- (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;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

/**
将需要执行的任务放到主线程进行操作

  • aSelector:方法Id
  • arg:需要传的参数
  • wait:指定,当前线程是否要被阻塞,直到主线程将我们制定的代码块(RefreshCellForLiveId:方法)执行完
  • array:暂时未找到用法
    注意:
    1.当前线程为主线程的时候,waitUntilDone:YES参数无效。
    2.该方法,没有返回值
    3.该方法主要用来用主线程来修改页面UI的状态。
    */
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"currentThread:%@",[NSThread currentThread]);
        [self performSelectorOnMainThread:@selector(text_thread:) withObject:@"指定到主线程操作" waitUntilDone:YES];
    });
}
- (void)text_thread:(id)obj{
    NSLog(@"obj:%@\ncurrentThread:%@",obj,[NSThread currentThread]);
}

打印数据:
2018-04-26 17:08:12.827038+0700 GCD[81044:4152136] currentThread:{number = 3, name = (null)}
2018-04-26 17:08:12.830341+0700 GCD[81044:4152040] obj:指定到主线程操作
currentThread:{number = 1, name = main}

指定任务线程到指定的线程上执行

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array 
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait

指定任务到后台执行

- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg 

线程枷锁

线程同步 线程和其他线程可能会共享一些资源,当多个线程同时读写同一份共享资源的时候,可能会引起冲突。线程同步是指是指在一定的时间内只允许某一个线程访问某个资源
iOS实现线程加锁有NSLock和@synchronized, dispatch_semaphore 三种方式

销售火车票

- (void)viewDidLoad {
    [super viewDidLoad];
  //先监听线程退出的通知,以便知道线程什么时候退出
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(threadExitNotice) name:NSThreadWillExitNotification object:nil];
    _ticketCount = 10;
    //新建两个子线程(代表两个窗口同时销售门票)
    NSThread * window1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    window1.name = @"北京售票窗口";
    [window1 start];
    
    NSThread * window2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    window2.name = @"广州售票窗口";
    [window2 start];
}

- (void)threadExitNotice{
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"%@",[NSThread currentThread]);
    [NSThread exit];
}

//线程启动后,执行saleTicket,执行完毕后就会退出,为了模拟持续售票的过程,我们需要给它加一个循环
- (void)saleTicket {
    while (1) {
        //如果还有票,继续售卖
        if (_ticketCount > 0) {
            _ticketCount --;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
            [NSThread sleepForTimeInterval:0.2];
        }
        //如果已卖完,关闭售票窗口
        else {
            break;
        }
    }
}

打印数据:
2018-04-26 21:55:03.736479+0700 GCDCC[41894:2658144] 剩余票数:8 窗口:广州售票窗口
2018-04-26 21:55:03.736480+0700 GCDCC[41894:2658143] 剩余票数:9 窗口:北京售票窗口
2018-04-26 21:55:03.939814+0700 GCDCC[41894:2658143] 剩余票数:7 窗口:北京售票窗口
2018-04-26 21:55:03.946772+0700 GCDCC[41894:2658144] 剩余票数:6 窗口:广州售票窗口
2018-04-26 21:55:04.151320+0700 GCDCC[41894:2658143] 剩余票数:5 窗口:北京售票窗口
2018-04-26 21:55:04.156364+0700 GCDCC[41894:2658144] 剩余票数:4 窗口:广州售票窗口
2018-04-26 21:55:04.352419+0700 GCDCC[41894:2658143] 剩余票数:3 窗口:北京售票窗口
2018-04-26 21:55:04.359034+0700 GCDCC[41894:2658144] 剩余票数:2 窗口:广州售票窗口
2018-04-26 21:55:04.554506+0700 GCDCC[41894:2658143] 剩余票数:1 窗口:北京售票窗口
2018-04-26 21:55:04.563299+0700 GCDCC[41894:2658144] 剩余票数:0 窗口:广州售票窗口
2018-04-26 22:06:17.331139+0700 GCDCC[42004:2665469] {number = 3, name = 北京售票窗口}
2018-04-26 22:06:17.535072+0700 GCDCC[42004:2665470] {number = 4, name = 广州售票窗口}

总结:可以看到,票的销售过程中出现了剩余数量错乱的情况,这就是前面提到的线程同步问题。

售票是一个典型的需要线程同步的场景,由于售票渠道有很多,而票的资源是有限的,当多个渠道在短时间内卖出大量的票的时候,如果没有同步机制来管理票的数量,将会导致票的总数和售出票数对应不上的错误。

优化一

- (void)viewDidLoad {
    [super viewDidLoad];
    //先监听线程退出的通知,以便知道线程什么时候退出
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(threadExitNotice) name:NSThreadWillExitNotification object:nil];
    _ticketCount = 10;
    //新建两个子线程(代表两个窗口同时销售门票)
    NSThread * window1 = [[NSThread alloc]initWithTarget:self selector:@selector(thread1) object:nil];
    [window1 start];
    
    NSThread * window2 = [[NSThread alloc]initWithTarget:self selector:@selector(thread2) object:nil];
    [window2 start];
    [self performSelector:@selector(saleTicket) onThread:window1 withObject:nil waitUntilDone:NO];
    [self performSelector:@selector(saleTicket) onThread:window2 withObject:nil waitUntilDone:NO];
    
}
- (void)threadExitNotice{
    NSLog(@"%@",[NSThread currentThread]);
    [NSThread exit];
}
//接着我们给线程创建一个runLoop
- (void)thread1 {
    [NSThread currentThread].name = @"北京售票窗口";
    NSRunLoop * runLoop1 = [NSRunLoop currentRunLoop];
    [runLoop1 runUntilDate:[NSDate date]]; //一直运行
}
- (void)thread2 {
    [NSThread currentThread].name = @"广州售票窗口";
    NSRunLoop * runLoop2 = [NSRunLoop currentRunLoop];
    [runLoop2 runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //自定义运行时间
}
- (void)saleTicket {
    while (1) {
        @synchronized(self) {
            //如果还有票,继续售卖
            if (_ticketCount > 0) {
                _ticketCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            //如果已卖完,关闭售票窗口
            else {
                if ([NSThread currentThread].isCancelled) {
                    break;
                }else {
                    NSLog(@"售卖完毕");
                    //给当前线程标记为取消状态
                    [[NSThread currentThread] cancel];
                    //停止当前线程的runLoop
                    CFRunLoopStop(CFRunLoopGetCurrent());
                }
            }
        }
    }
}

打印数据:
2018-04-26 22:19:41.665526+0700 GCDCC[42140:2674733] 剩余票数:9 窗口:北京售票窗口
2018-04-26 22:19:41.866523+0700 GCDCC[42140:2674734] 剩余票数:8 窗口:广州售票窗口
2018-04-26 22:19:42.067599+0700 GCDCC[42140:2674733] 剩余票数:7 窗口:北京售票窗口
2018-04-26 22:19:42.270942+0700 GCDCC[42140:2674734] 剩余票数:6 窗口:广州售票窗口
2018-04-26 22:19:42.473745+0700 GCDCC[42140:2674733] 剩余票数:5 窗口:北京售票窗口
2018-04-26 22:19:42.674902+0700 GCDCC[42140:2674734] 剩余票数:4 窗口:广州售票窗口
2018-04-26 22:19:42.878208+0700 GCDCC[42140:2674733] 剩余票数:3 窗口:北京售票窗口
2018-04-26 22:19:43.080097+0700 GCDCC[42140:2674734] 剩余票数:2 窗口:广州售票窗口
2018-04-26 22:19:43.280505+0700 GCDCC[42140:2674733] 剩余票数:1 窗口:北京售票窗口
2018-04-26 22:19:43.483755+0700 GCDCC[42140:2674734] 剩余票数:0 窗口:广州售票窗口
2018-04-26 22:19:43.687034+0700 GCDCC[42140:2674733] 售卖完毕
2018-04-26 22:19:43.687204+0700 GCDCC[42140:2674734] 售卖完毕
2018-04-26 22:19:43.687649+0700 GCDCC[42140:2674733] {number = 3, name = 北京售票窗口}
2018-04-26 22:19:43.687915+0700 GCDCC[42140:2674734] {number = 4, name = 广州售票窗口}

如果确定两个线程都是isCancelled状态,可以调用[NSThread exit]方法来终止线程。

优化二

@interface ViewController ()
{
    NSInteger  _ticketCount;
    NSLock *lock;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    //先监听线程退出的通知,以便知道线程什么时候退出
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(threadExitNotice) name:NSThreadWillExitNotification object:nil];
    _ticketCount = 10;
    lock = [[NSLock alloc]init];
    //新建两个子线程(代表两个窗口同时销售门票)
    NSThread * window1 = [[NSThread alloc]initWithTarget:self selector:@selector(thread1) object:nil];
    [window1 start];
    
    NSThread * window2 = [[NSThread alloc]initWithTarget:self selector:@selector(thread2) object:nil];
    [window2 start];
    [self performSelector:@selector(saleTicket) onThread:window1 withObject:nil waitUntilDone:NO];
    [self performSelector:@selector(saleTicket) onThread:window2 withObject:nil waitUntilDone:NO];
    
}
- (void)threadExitNotice{
    NSLog(@"%@",[NSThread currentThread]);
    [NSThread exit];
}
//接着我们给线程创建一个runLoop
- (void)thread1 {
    [NSThread currentThread].name = @"北京售票窗口";
    NSRunLoop * runLoop1 = [NSRunLoop currentRunLoop];
    [runLoop1 runUntilDate:[NSDate date]]; //一直运行
}
- (void)thread2 {
    [NSThread currentThread].name = @"广州售票窗口";
    NSRunLoop * runLoop2 = [NSRunLoop currentRunLoop];
    [runLoop2 runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10.0]]; //自定义运行时间
}
- (void)saleTicket {
    while (1) {
        [lock lock];
        //如果还有票,继续售卖
        if (_ticketCount > 0) {
            _ticketCount --;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
            [NSThread sleepForTimeInterval:0.2];
        }
        //如果已卖完,关闭售票窗口
        else {
            if ([NSThread currentThread].isCancelled) {
                break;
            }else {
                NSLog(@"售卖完毕");
                //给当前线程标记为取消状态
                [[NSThread currentThread] cancel];
                //停止当前线程的runLoop
                CFRunLoopStop(CFRunLoopGetCurrent());
            }
        }
    //解锁
    [lock unlock];
    }
}

优化三 请参考我的文章

GCDiOS GCD详 线程加锁方式
https://www.jianshu.com/p/97ed78a6f9b8

你可能感兴趣的:(iOS NSThread进阶 详)