小笨狼漫谈多线程:NSThread

多线程是程序开发中非常基础的一个概念,大家在开发过程中应该或多或少用过相关的东西。同时这恰恰又是一个比较棘手的概念,一切跟多线程挂钩的东西都会变得复杂。如果使用过程中对多线程不够熟悉,很可能会埋下一些难以预料的坑。

iOS中的多线程技术主要有NSThread, GCD和NSOperation。他们的封装层次依次递增,其中

  • NSThread封装性最差,最偏向于底层,主要基于thread使用
  • GCD是基于C的API,直接使用比较方便,主要基于task使用
  • NSOperation是基于GCD封装的NSObject对象,对于复杂的多线程项目使用比较方便,主要基于队列使用

这篇文章是这个多线程系列的第一篇,主要介绍NSThread, NSThread是上面三项技术中唯一基于thread的,每一个NSThread对象代表着一个线程,理解NSThread更有利于理解多线程的含义

多线程的概念

曾经面试的时候被问到过什么是线程和进程?当时感觉自己似乎知道这是什么东西,但是比划了半天就是说不上来

根据Apple官方的定义:

The term thread is used to refer to a separate path of execution for code.
The term process is used to refer to a running executable, which can encompass multiple threads.
  • 线程用于指代一个独立执行的代码路径
  • 进程用于指代一个可执行程序,他可以包含多个线程

当一个可执行程序中拥有多个独立执行的代码路径的时候,这就叫做多线程


NSThread API

线程创建

对于NSThread来说,每一个对象就代表着一个线程,NSThread提供了2种创建线程的方法:

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
  • detach方法直接创建并启动一个线程去Selector,由于没有返回值,如果需要获取新创建的Thread,需要在执行的Selector中调用-[NSThread currentThread]获取
  • init方法初始化线程并返回,线程的入口函数由Selector传入。线程创建出来之后需要手动调用-start方法启动

线程操作

创建好线程之后当然需要对线程进行操作,NSThread给线程提供的主要操作方法有启动,睡眠,取消,退出

启动

我们使用init方法将线程创建出来之后,线程并不会立即运行,只有我们手动调用-start方法才会启动线程

- (void)start NS_AVAILABLE(10_5, 2_0);

这里要注意的是:部分线程属性需要在启动前设置,线程启动之后再设置会无效。如qualityOfService属性

睡眠

NSThread提供了2个让线程睡眠的方法,一个是根据NSDate传入睡眠时间,一个是直接传入NSTimeInterval

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

看到sleepUntilDate:大家可能会想起runloop的runUntilDate:。他们都有阻塞线程的效果,但是阻塞之后的行为又有不一样的地方,使用的时候,我们需要根据具体需求选择合适的API。

  • sleepUntilDate:相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务
  • runUntilDate:虽然会阻塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate:之后的代码

取消

对于线程的取消,NSThread提供了一个取消的方法和一个属性

@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

- (void)cancel NS_AVAILABLE(10_5, 2_0);

不过大家千万不要被它的名字迷惑,调用-cancel方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在main函数中自己实现

要实现取消的功能,我们需要自己在线程的main函数中定期检查isCancelled状态来判断线程是否需要退出,当isCancelled为YES的时候,我们手动退出。如果我们没有在main函数中检查isCancelled状态,那么调用-cancel将没有任何意义

退出

与充满不确定性的-cancel相比,-exit函数可以让线程立即退出。

+ (void)exit;

-exit属于核弹级别终极API,调用之后会立即终止线程,即使任务还没有执行完成也会中断。这就非常有可能导致内存泄露等严重问题,所以一般不推荐使用。

对于有runloop的线程,可以使用CFRunLoopStop()结束runloop配合-cancel结束线程

[2016.1.19更新]
感谢@NSHYJ的提醒。runloop启动的方法中runrunUntilDate:都无法使用CFRunLoopStop()退出,只有runMode:beforeDate:可以响应CFRunLoopStop(),所以要想使用CFRunLoopStop()退出runloop,必须使用runMode:beforeDate:启动

线程通讯

线程准备好之后,经常需要从主线程把耗时的任务丢给辅助线程,当任务完成之后辅助线程再把结果传回主线程传,这些线程通讯一般用的都是perform方法

//①
- (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 NS_AVAILABLE(10_5, 2_0);
//④
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0); 
  • ①:将selector丢给主线程执行,可以指定runloop mode
  • ②:将selector丢给主线程执行,runloop mode默认为common mode
  • ③:将selector丢个指定线程执行,可以指定runloop mode
  • ④:将selector丢个指定线程执行,runloop mode默认为default mode

所以我们一般用③④方法将任务丢给辅助线程,任务执行完成之后再使用①②方法将结果传回主线程。

注意:perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行。

线程优先级

每个线程的紧急程度是不一样的,有的线程中任务你也许希望尽快执行,有的线程中任务也许并不是那么紧急,所以线程需要有优先级。优先级高线程中的任务会比优先级低的线程先执行

NSThread有4个优先级的API

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started
  • 前2个是类方法,用于设置和获取当前线程的优先级
  • threadPriority属性可以通过对象设置和获取优先级
  • 由于线程优先级是一个比较抽线的东西,没人能知道0.5和0.6到底有多大区别,所以iOS8之后新增了qualityOfService枚举属性,大家可以通过枚举值设置优先级
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceDefault = -1
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
}

NSQualityOfService主要有5个枚举值,优先级别从高到低排布:

  • NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
  • NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回的任务
  • NSQualityOfServiceDefault:默认优先级,当没有设置优先级的时候,线程默认优先级
  • NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回的任务
  • NSQualityOfServiceBackground:后台优先级,用于完全不紧急的任务

一般主线程和没有设置优先级的线程都是默认优先级

主线程和当前线程

NSThread也提供了非常方便的获取和判断主线程的API

@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);
  • isMainThread:判断当前线程是否是主线程
  • mainThread:获取主线程的thread

除了获取主线程,我们也可以使用-currentThread获取当前线程

+ (NSThread *)currentThread;

线程通知

NSThread有三个线程相关的通知

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

NSThread实例

只看API毕竟比较抽象,下面我用一个例子给大家展示NSThread的使用方法

线程创建

我们首先来创建一个线程,并用self.thread持有,以便后面操作线程和线程通讯使用

self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil]; // ①创建线程
self.thread.qualityOfService = NSQualityOfServiceDefault; //②设置线程优先级
[self.thread start]; //③启动线程
  • ①:创建线程,并指定入口main函数为-threadMain
  • ②:设置线程的优先级,qualityOfService属性必须在线程启动之前设置,启动之后将无法再设置
  • ③:调用start方法启动线程。

由于线程的创建和销毁非常消耗性能,大多情况下,我们需要复用一个长期运行的线程来执行任务。

在线程启动之后会首先执行-threadMain,正常情况下threadMain方法执行结束之后,线程就会退出。为了线程可以长期复用接收消息,我们需要在threadMain中给thread添加runloop

- (void)threadMain {
    [[NSThread currentThread] setName:@"myThread"]; // ①给线程设置名字
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];    // ②给线程添加runloop
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];   //③给runloop添加数据源
    while (![[NSThread currentThread] isCancelled]) {           //④:检查isCancelled
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];  //⑤启动runloop
    }
}
  • ①:设置线程的名字,这一步不是必须的,主要是为了debug的时候更方便,可以直接看出这是哪个线程
  • ②:自定义的线程默认是没有runloop的,调用-currentRunLoop,方法内部会为线程创建runloop
  • ③:如果没有数据源,runloop会在启动之后会立刻退出。所以需要给runloop添加一个数据源,这里添加的是NSPort数据源
  • ④:定期检查isCancelled,当外部调用-cancel方法将isCancelled置为YES的时候,线程可以退出
  • ⑤:启动runloop

线程通讯

线程创建好了之后我们就可以给线程丢任务了,当我们有一个需要比较耗时的任务的时候,我们可以调用perform方法将task丢给这个线程。

[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO]

结束线程

当我们想要结束线程的时候,我们可以使用CFRunLoopStop()配合-cancel来结束线程。

- (void)cancelThread
{
    [[NSThread currentThread] cancel];
    CFRunLoopStop(CFRunLoopGetCurrent());
}

不过这个方法必须在self.thread线程下调用。如果当前是主线程。可以perform到self.thread下调用这个方法结束线程

Extension

About Me

个人博客 简书 微博 QQ群:159974494

欢迎大家关注我,共同学习iOS,如果你觉得我写得不错,可以打赏我5毛钱,哈哈~

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

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,665评论 1 17
  • Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么? 1...
    AlanGe阅读 1,652评论 0 17
  • 本文选译自《Threading Programming Guide》。 导语 线程技术作为在单个应用程序中并发执行...
    巧巧的二表哥阅读 2,380评论 4 24
  • (一) 他被几个人念着一,二,三小心翼翼抬下了移动铁床,安置在病床上。­喉咙上,鼻上,手腕,下腹部,尿道上,插满了...
    我叫她小胖子阅读 594评论 0 0
  • 许久,父亲吐出一串烟圈,然后缓缓掐灭烟头,接着,又慢慢点燃一支,深深地吸了一口。残阳透过窗户,照在父亲苍老的脸上,...
    天铎1916阅读 249评论 1 2