iOS 多线程: NSThread的介绍

NSThread的封装性在多线程中是最差的, 最偏向于底层, 主要基于thread使用

一: 多线程的概念:

线程: 指一个独立执行的代码路径(比如: 一个工作的人)
进程: 指一个可执行程序, 它可以包含多个线程(比如: 一个运行的部门)
当一个可执行程序中拥有多个独立执行的代码路径的时候, 这就叫做多线程

二: 线程的创建:

对于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;

● detach方法: 直接创建并启动一个线程去执行方法, 由于没有返回值, 如果需要获取新创建的线程, 需要在执行的selector方法中调用-[NSThread currentThread]获取
● init方法: 初始化线程, 并返回, 线程的入口函数由selector传入, 线程创建出来之后需要手动调用-start方法启动

三: 线程的操作:

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

1: 启动

我们使用init方法将线程创建出来之后, 线程并不会立即运行, 只有我们手动调用-start方法之后才会启动线程:(- detachNewThreadSelector: toTarget: withObject: 是个例外)

- (void)start;

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

2: 睡眠

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

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

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

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

3: 取消

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

@property (readonly, getter=isCancelled) BOOL cancelled;

- (void)cancel;

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

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

示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//开启线程
[thread start];
//睡眠3秒
[NSThread sleepForTimeInterval:3];
//取消线程
[thread cancel];
//判断是否已经取消
if (thread.isCancelled) {
    CCLog(@"线程已经取消");
}
else {
    CCLog(@"线程没有取消");
}

- (void)demo {
    CCLog(@"1-----");
    for (int i = 0; i < 5; i++) {
        //睡眠1秒
        [NSThread sleepForTimeInterval:1];
        CCLog(@"第%d个索引",i);
    }
    //判断是否已经取消
    if (NSThread.currentThread.isCancelled) {
        return;
    }
    CCLog(@"2-----");
}


输出结果:
1——
第0个索引
第1个索引
线程已经取消
第2个索引
第3个索引
第4个索引

可以看到:在取消子线程之后, 子线程仍然在执行, 直到判断子线程已经取消后, 我们手动终止线程, 子线程才停止

4: 退出

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

+ (void)exit;

+exit属于核弹级别的终极API, 调用之后会立即终止线程, 即使任务还没有执行完成也会中断, 这就非常有可能导致内存泄漏等严重问题, 所以一般不推荐使用.
对于有runloop的线程, 可以使用CFRunLoopStop()结束runloop配合-cancel结束线程

示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//开启线程
[thread start];

- (void)demo {
    CCLog(@"1-----");
    for (int i = 0; i < 5; i++) {
        //睡眠1秒
        [NSThread sleepForTimeInterval:1];
        if (i == 3) {
            [NSThread exit];
        }
        CCLog(@"第%d个索引",i);
    }
    CCLog(@"2-----");
}

输出:
1——
第0个索引
第1个索引
第2个索引

四: 线程通讯

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

①
- (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)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray *)array API_AV;
④
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

①: 将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;
@property NSQualityOfService qualityOfService;

前两个是类方法, 用于设置和获取当前线程的优先级
threadPriority属性可以通过对象来设置和获取优先级
由于线程优先级是一个比较抽象的东西, 没人知道0.5和0.6到底有多大区别, 所以iOS8之后新增了qualityOfService枚举属性, 大家可以通过枚举值设置优先级

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21, 
    NSQualityOfServiceUserInitiated = 0x19,  
    NSQualityOfServiceUtility = 0x11,         
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
};

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

  • NSQualityOfServiceUserInteractive: 最高优先级, 主要用于直接参与提供交互式UI的工作, 例如: 处理点击事件或绘制图像到屏幕上
  • NSQualityOfServiceUserInitiated: 次高优先级, 用于执行用户明确请求的工作, 并且必须立即显示结果, 以便进行进一步的用户交互
  • NSQualityOfServiceDefault: 默认优先级, 当没有设置优先级的时候, 线程默认优先级
  • NSQualityOfServiceUtility: 普通优先级,主要用于不需要立即返回的任务
  • NSQualityOfServiceBackground: 后台优先级, 用于完全不紧急的任务

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

六: 主线程和当前线程:

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

@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread;
@property (class, readonly, strong) NSThread *mainThread;
  • isMainThread: 判断当前线程是否是主线程;
  • mainThread: 获取主线程的thread

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

@property (class, readonly, strong) NSThread *currentThread;

七: 线程通知:

NSThread有三个线程相关的通知:

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

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

八: NSThread实例:

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

1: 线程的创建:

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

//①创建线程
self.thred = [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 runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10];//⑤启动runloop
    }
}

①: 设置线程的名字, 这一步不是必须的, 主要是为了debug的时候更方便, 可以直接看出这是哪个线程
②: 自定义的线程默认是没有runloop的, 调用- currentRunLoop, 方法内部会为线程创建runloop
③: 如果没有数据源, runloop会在启动之后立刻退出, 所以需要给runloop添加一个数据源, 这里添加的是NSPort数据源
④: 定期检查isCancelled, 当外部调用-cabncel方法将isCancelled设置为YES的时候, 线程可以退出;
⑤: 启动runloop

2: 线程通讯:

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

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

3: 结束线程:

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

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

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

原文:https://www.open-open.com/lib/view/open1452993005808.html#articleHeader14

你可能感兴趣的:(iOS 多线程: NSThread的介绍)