NSThead的进阶使用和简单探讨

image

概述

NSThread类是一个继承于NSObjct类的轻量级类。一个NSThread对象就代表一个线程。它需要管理线程的生命周期、同步、加锁等问题,因此会产生一定的性能开销。
使用NSThread类可以在特定的线程中被调用某个OC方法。当需要执行一个冗长的任务,并且不想让这个任务阻塞应用中的其他部分,尤其为了避免阻塞app的主线程(因为主线程用于处理用户界面展示交互和事件相关的操作),这个时候非常适合使用多线程。线程也可以将一个庞大的任务分为几个较小的任务,从而提高多核计算机的性能。

NSThread类在运行期监听一个线程的语义和NSOperation类是相似的。比如取消一个线程或者决定一个任务执行完后这个线程是否存在。

本文将会从这几个方面开始探讨NSThread

image

方法属性的介绍


初始化(创建)一个NSThread对象

// 返回一个初始化的NSThread对象
- (instancetype)init
// 返回一个带有多个参数的初始化的NSThread对象
// selector :线程执行的方法,最多只能接收一个参数
// target :selector消息发送的对象
// argument : 传给selector的唯一参数,也可以是nil
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument );
// iOS 10
- (instancetype)initWithBlock:(void (^)(void))block;

启动一个线程。

// 开辟一个新的线程,并且使用特殊的选择器Selector作为线程入口,调用完毕后,会马上创建并开启新线程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
 // iOS 10
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
// 启动接受者
- (void)start;
// 线程体方法,线程主要入口,start 后执行
// 该方法默认实现了目标(target)和选择器(selector),用于初始化接受者和调用指定目标(target)的方法。如果子类化NSThread,需要重写这个方法并且用它来实现这个线程主体。在这种情况下,是不需要调用super方法的。
// 不应该直接调用这个方法。你应该通过调用启动方法开启一个线程。
- (void)main;

使用initWithTarget:selector:initWithBlock:detachNewThreadSelector:detachNewThreadWithBlock:创建线程都是异步线程。

停止一个线程

// 阻塞当前线程,直到特定的时间。
+ (void)sleepUntilDate:(NSDate *)date;
// 让线程处于休眠状态,直到经过给定的时间间隔
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 终止当前线程
+ (void)exit;
// 改变接收者的取消状态,来表示它应该终止
- (void)cancel;

决定线程状态

// 接收者是否存在
@property (readonly, getter=isExecuting) BOOL executing;
// 接收者是否结束执行
@property (readonly, getter=isFinished) BOOL finished;
// 接收者是否取消
@property (readonly, getter=isCancelled) BOOL cancelled;

主线程相关

// 当前线程是否是主线程
@property (class, readonly) BOOL isMainThread;
// 接受者是否是主线程
@property (readonly) BOOL isMainThread;
// 获取主线程的对象
@property (class, readonly, strong) NSThread *mainThread;

执行环境

// 这个app是否是多线程
+ (BOOL)isMultiThreaded;
// 返回当前执行线程的线程对象。
@property (class, readonly, strong) NSThread *currentThread;
// 返回一个数组,包括回调堆栈返回的地址
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses ;
// 返回一个数组,包括回调堆栈信号
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;

线程属性相关

// 线程对象的字典
@property (readonly, retain) NSMutableDictionary *threadDictionary;

NSAssertionHandlerKey
// 接收者的名字
@property (nullable, copy) NSString *name;
// 接收者的对象大小,以byte为单位
@property NSUInteger stackSize;

线程优先级

// 线程开启后是个只读属性
@property NSQualityOfService qualityOfService;
// 返回当前线程的优先级
+ (double)threadPriority;
// 接受者的优先级,已经废弃,使用qualityOfService代替
@property double threadPriority;
// 设置当前线程的优先级。设置线程的优先级(0.0 - 1.0,1.0最高级)
+ (BOOL)setThreadPriority:(double)p;

通知

// 未被实现,没有实际意义,保留项
NSDidBecomeSingleThreadedNotification
// 在线程退出前,一个NSThread对象收到到退出消息时会发送这个通知。
NSThreadWillExitNotification
// 当第一个线程启动时会发送这个通知。这个通知最多发送一次。当NSThread第一次发送用`detachNewThreadSelector:toTarget:withObject:`,`detachNewThreadWithBlock:`,`start`消息时,发送通知。后续调用这些方法是不会发送通知。
NSWillBecomeMultiThreadedNotification

线程间通信,
在NSObject的分类NSThreadPerformAdditions中的方法(NSThread.h文件中)具有这些特性:

  1. 无论是在主线程还是在子线程中都可执行,并且均会调用主线程的aSelector方法;
  2. 方法是异步的
@interface NSObject (NSThreadPerformAdditions)
// 如果设置wait为YES: 等待当前线程执行完以后,主线程才会执行aSelector方法;
// 如果设置wait为NO:不等待当前线程执行完,就在主线程上执行aSelector方法。
// 如果,当前线程就是主线程,那么aSelector方法会马上执行,wait是YES参数无效。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;

// 等于第一个方法中modes是kCFRunLoopCommonModes的情况。指定了线程中 Runloop 的 Modes =  kCFRunLoopCommonModes。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

// 在指定线程上操作,因为子线程默认未添加NSRunloop,在线程未添加runloop时,是不会调用选择器中的方法的。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:( NSArray<NSString *> *)array ;
// 等于第一个方法中modes是kCFRunLoopCommonModes的情况。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait ;

// 隐式创建子线程,在后台创建。并且是个同步线程。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg ;
@end

直接给接受者发消息的其他方法。

  1. 协议NSObject中的方法,可在主线程或者子线程执行。因为是在当前线程执行的同步任务,因此会阻塞当前线程。这几个方法等同于直接调用方法。
// 当前线程操作。
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
  1. 延迟操作&按照顺序操作

NSRunLoop.h文件中

// 延迟操作
/****************   Delayed perform  ******************/

@interface NSObject (NSDelayedPerforming)
// 异步方法,不会阻塞当前线程,只能在主线程中执行。是把`Selector`加到主队列里,当 `delay`之后执行`Selector`。如果主线程在执行业务,那只能等到执行完所有业务之后才会去执行`Selector`,就算`delay`等于 0。
// 那`delay `从什么时候开始计算呢?从发送`performSelector`消息的时候。就算这时主线程在阻塞也会计算时间,当阻塞结束之后,如果到了`delay`那就执行`Selector`,如果没到就继续 `delay`。
// 只能在主线程中执行,在子线程中不会调到aSelector方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
// 等于第一个方法中modes是kCFRunLoopCommonModes的情况。指定了线程中 Runloop 的 Modes =  kCFRunLoopCommonModes。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
// 在方法未到执行时间之前,取消方法。调用这2个方法当前target执行dealloc之前,以确保不会Crash。
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end
// 按照排序顺序执行
@interface NSRunLoop (NSOrderedPerform)
// 按某种顺序order执行方法。参数order越小,优先级越高,执行越早
// selector都是target的方法,argument都是target的参数
// 这2个方法会设置一个定时器去在下个runloop循环的开始时让target执行aSelector消息。 定时器根据modes确认模式。当定时器触发,定时器尝试队列从runloop中拿出消息并执行。
如果run loop 正在运行,并且是指定modes的一种,则是成功的,否则定时器一直等待直到runloop是modes 中的一种。
- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;

@end

本文介绍大部分的知识点如思维导图:


image

使用

  1. 创建线程
    用initXXX初始化的需要调用start方法来启动线程。而detachXXX初始化方法,直接启动线程。这个2中方式创建的线程都是显式创建线程。
//1. 手动开启,action-target 方式
NSThread * actionTargetThread = [[NSThread alloc] initWithTarget:self selector:@selector(add:) object:nil];
[actionTargetThread start];
//2. 手动开启, block 方式
NSThread *blockThread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"%s",__func__);
}];
[blockThread start];
//3. 创建就启动, action-target 方式
[NSThread detachNewThreadSelector:@selector(add2:) toTarget:self withObject:@"detachNewThreadSelector"];
//4. 创建就启动, block 方式
[NSThread detachNewThreadWithBlock:^{
    NSLog(@"%s",__func__);
}];
  1. 线程中通信

2.1 NSThreadPerformAdditions分类方法,异步调用方法
// 无论在子线程还是主线程,都会调用主线程方法。

a. 主线程

    [self performSelectorOnMainThread:@selector(add:) withObject:nil waitUntilDone:YES];
    //[self performSelectorOnMainThread:@selector(add:) withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];

子线程默认没有开启runloop。需要手动添加,不然选择器方法无法调用。

b. 子线程

使用initWithBlock:方式创建。

//1. 开辟一个子线程
NSThread *subThread1 = [[NSThread alloc] initWithBlock:^{
  // 2.子线程方法中添加runloop
  // 3.实现线程方法
    [[NSRunLoop currentRunLoop] run];
}];
//1.2. 启动一个子线程
[subThread1 start];
// 2. 在子线程中调用方法
// [self performSelector:@selector(add:) onThread:subThread1 withObject:@"22" waitUntilDone:YES];
[self performSelector:@selector(add:) onThread:subThread1 withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];

使用initWithTarget:selector:object:创建。

// 1. 开辟一个子线程
NSThread *subThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
// 1.2 启动一个子线程
[subThread2 start];
// 3. 在子线程中调用方法
// [self performSelector:@selector(add:) onThread:subThread2 withObject:@"22" waitUntilDone:YES];
[self performSelector:@selector(add:) onThread:subThread1 withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
// 2.子线程方法中添加runloop
- (void)startThread{
    [[NSRunLoop currentRunLoop] run];
}

c. 后台线程(隐式创建一个线程)

[self performSelectorInBackground:@selector(add:) withObject:@"arg"];

2.2 协议NSObject方法
创建是的同步任务。

[NSThread detachNewThreadWithBlock:^{
    // 直接调用
    [self performSelector:@selector(add:) withObject:@"xxx"];
}];

2.3 延迟
NSObject分类NSDelayedPerforming方法,添加异步任务,并且是在主线程上执行。

[self performSelector:@selector(add:) withObject:self afterDelay:2];

2.4 按照顺序操作
NSRunLoop分类NSOrderedPerform中的方法

[NSThread detachNewThreadWithBlock:^{
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
    // 记得添加端口。不然无法调用selector方法
    [currentRunloop addPort:[NSPort port] forMode:(NSRunLoopMode)kCFRunLoopCommonModes];
    [currentRunloop performSelector:@selector(add:) target:self argument:@"arg1" order:1 modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
    [currentRunloop performSelector:@selector(add:) target:self argument:@"arg3" order:3 modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
    [currentRunloop run];
}];

线程安全

问题:

多个线程可能会同时访问同一块资源。比如多个线程同时访问同一个对象、同一个变量、同一个文件等。当多个线程同时抢夺同一个资源,会引起线程不安全性,可能会造成数据错乱和数据安全问题。

解决:

使用线程同步技术: 可以对可能会被抢夺的资源,在被被竞争的时候加锁。让其保证线程同步状态。而锁具有多种类型:比如读写锁、自旋锁、互斥锁、信号量、条件锁等。在NSThread可能造成资源抢夺情况下,可以使用互斥锁。互斥锁就是多个线程任务按顺序的执行。
如下就使用的情况之一:对需要读写操作的资源,进行加锁操作。

for (NSInteger index = 0 ; index < 100; index ++) {
    @synchronized (self) {
        self.allCount -= 5;
        NSLog(@"%@卖出了车票,还剩%ld",[NSThread currentThread].name,self.allCount);
    }
}

线程生命周期。

线程的生命周期是:新建 - 就绪 - 运行 - 阻塞 - 死亡。当线程启动后,它不能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也就会随之改变。

image
  1. 新建和就绪状态
    �显式创建,使用initWithTarget:selector:initWithBlock:创建一个线程,未启动,只有发送start消息才会启动,然后处于就行状态。
    使用detachNewThreadWithBlock:detachNewThreadSelector:toTarget:显示创建并立即启动。 还有种创建方式,隐式创建并立即启动:performSelectorInBackground:withObject:

  2. 运行和阻塞状态
    如果处于就绪状态的线程获得了CPU资源,开始执行可执行方法的线程执行体(block或者@Selector),则该线程处于运行状态。

当发生如下情况下,线程将会进入阻塞状态:

  • 线程调用sleep方法:sleepUntilDate: sleepForTimeInterval:主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
    线程试图获得一个同步监视器,但该同步监视器正被其他线程锁持有。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的suspend方法将该线程挂起。不过这个方法容易导致死锁,所以程序应该尽量避免使用该方法。
      当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会了。被阻塞的线程会在合适时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是
    说被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
    针对上面的几种情况,当发生如下特定的情况将可以解除上面的阻塞,让该线程重新进入就绪状态:
  • 调用sleep方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了resume恢复方法。
  1. 线程死亡
  • 可执行方法执行完成,线程正常结束。
  • 程序的意外奔溃。
  • 该线程的发送exit消息来结束该线程。
// 1. 创建:New状态
NSThread * actionTargetThread = [[NSThread alloc] initWithTarget:self selector:@selector(add:) object:nil];
// 2. 启动:就绪状态
[actionTargetThread start];
// 可执行方法
- (void)add:(id)info{
    // 3. 执行状态
    NSLog(@"%s,info %@",__func__,info);
    // 5. 当前线程休眠
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"after");
    // 4. 程序正常退出
}
// 6. 打取消标签
[actionTargetThread cancel];
// 7. 主动退出
[NSThread exit];

注意:

  • NSThread 管理多个线程比较困难,所以不太推荐在多线程任务多的情况下使用。
  • 苹果官方推荐�使用GCD和NSOperation。
  • [NSTread currentThread] 跟踪任务所在线程,适用于NSTread,NSOperation,和GCD
  • 用NSThread创建的线程,不会自动添加autoreleasepool

参考

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

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,258评论 8 265
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,224评论 4 56
  • iOS多线程实践中,常用的就是子线程执行耗时操作,然后回到主线程刷新UI。在iOS中每个进程启动后都会建立一个主线...
    jackyshan阅读 1,429评论 2 12
  • const对普通变量进行修饰,以表该变量在以后的使用中不被修改。 初始化 const 对象作用域 默认仅在当前文本...
    tarzipc阅读 320评论 0 0
  • 少年悄悄的走近,又悄悄的走远。时间悄悄的来,又悄悄的走。我悄悄的喜欢上你,又悄悄的埋藏心底。 01 ...
    9466阅读 251评论 0 0