多线程之NSThread

NSThread分析

初始化NSThread对象

- init
    返回一个NSThread对象
- initWithTarget:selector:object:
    返回一个根据给定参数初始化的NSThread对象
- initWithBlock:
    返回一个NSThread对象,iOS 10 以后可用    

- initWithTarget:selector:object:会持有targetobject,但不会像timer一样无法释放,这里....

启动线程

+ detachNewThreadSelector:toTarget:withObject:
    开辟一个子线程并执行select方法
+ detachNewThreadWithBlock:
    开辟一个子线程并执行block
- start
    开启线程
- main 
    线程的入口函数

+ detachNewThreadSelector:toTarget:withObject:+ detachNewThreadWithBlock:会创建一个子线程,并调用start方法,再执行完任务后自动调用exit类方法(断点可验证),退出线程,在执行过程中会持有targetobjectblock

当非主线程的第一个线程创建时,NSTread会发布一次此通知 --- 使用detachNewThreadSelector:toTarget:withObject: start 方法,创建子线程。在新线程开始执行之前会发送该通知

start方法会创建一个新的线程,并在新线程上调用NSThreadmian方法(子类可以重写该方法),main方法中的默认实现为:

// - initWithTarget:selector:object:一一对应的关系,显而易见
[target performSelector:selector withObject:arg];

停止线程

+ sleepUntilDate:
    直到给定的date之前,线程都是睡眠状态,此时runloop无法运行
+ sleepForTimeInterval:
    直到给定的时间间隔interval之前,线程都是睡眠状态
+ exit
    退出当前线程,再退出线程之前会发送一个NSThreadWillExitNotification通知
- cancel
    更改thread的取消状态,表达该线程如果已经被取消,则退出

决定线程的执行状态属性

@property(readonly, getter=isExecuting) BOOL executing;
    如果当前线程正在执行,则返回YES,否则为NO
@property(readonly, getter=isFinished) BOOL finished;
    如果当前线程已经执行完毕,则返回YES,否则为NO
@property(readonly, getter=isCancelled) BOOL cancelled;
    如果接收器已被取消,则为 YES,否则为 NO。
    如果线程支持取消,则应定期检查此属性,如果返回 YES 则退出。    

主线程相关属性

@property(class, readonly) BOOL isMainThread;
    如果当前线程是主线程,则返回YES,否则为NO
@property(readonly) BOOL isMainThread;
    如果消息接收者(线程)是主线程,则返回YES,否则为NO
@property(class, readonly, strong) NSThread *mainThread;
    返回主线程对象        

线程执行环境

+ 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 
    线程名字    
@property NSUInteger stackSize
    线程栈大小,必须是4kb的倍数。设置栈的大小,必须在线程启动之前才有效,在线程启动后设置的值并不会 真正 地生效    

线程优先级

qualityOfService
    在线程开启后,只读
+ threadPriority
    返回当前线程的优先级,0.0 ~ 1.0, 1.0为最高级。优先级有内核决定,具体值待定
+ setThreadPriority:
    设置线程的优先级,0.0 ~ 1.0, 1.0为最高级。设置成功返回YES

NSThread相关API

下列方法都来自NSObject的分类NSObject (NSThreadPerformAdditions)

主线程执行

- performSelectorOnMainThread:withObject:waitUntilDone:modes:
- performSelectorOnMainThread:withObject:waitUntilDone:

这两个方法都在主线程中执行,可以从子线程回到主线程中。waitUntilDoneYES表示阻塞线程,需要等待selector中代码运行完再往下执行;NO的话表示,不必等待selector中的代码执行完毕,直接往下执行。 waitUntilDoneNO时,表示不需要阻塞线程,需要依托于RunLoop- performSelector:target:argument:order:modes:实现

自定义线程执行

- performSelector:onThread:withObject:waitUntilDone:modes:
- performSelector:onThread:withObject:waitUntilDone:

1.当前线程与执行线程相同

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"1");
    [self performSelector:@selector(log) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
    NSLog(@"3");
});

- (void)log {
    NSLog(@"2");
}

打印结果 1 2 3。这里的waitUntilDoneYES,由上文可知,会阻塞线程,需要等待log中的代码执行完毕才能继续往下执行。

接着,就该waitUntilDoneNO

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"1");
    [self performSelector:@selector(log) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
    NSLog(@"3");
});

- (void)log {
    NSLog(@"2");
}

打印结果 1 3,此时,并不需要等待log方法执行完毕,所以1和3先打印,另外由于waitUntilDoneNO,所以log方法的执行就需要依靠当前线程的RunLoop来执行,由于该GCD会开辟一个子线程执行任务(子线程中RunLoop默认不开启),所以log并不会执行

如果想执行打印2的代码,需要进行线程保活操作,需要保证子线程中的RunLoop开启并且不能阻塞。

2.当前线程与执行线程不同

// self.thread是一个常驻线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelector:@selector(currentState) onThread:self.thread withObject:nil waitUntilDone:YES];
});

查询了很多资料这里没有明确的说明,GNUStep中也没有到具体的实现。但是利用callStackSymbols属性查看堆栈信息,可以发现走的是RunLoop中的source0类型处理

tip: 从调用栈中发现__CFRunLoopDoSource0这个函数的存在,但是在Objective-C版本的RunLoop源码中并没有它的存在,于是就去找了Swift版本的,果然有(项目是纯Objective-C的)。

waitUntilDone这里不管是YES或者NO,走的都是RunLoop的souce0处理方式,和上面有些不同的是,waitUntilDoneYES的时候,系统加了条件锁(GNU实现方式)来实现阻塞线程的效果

后台线程

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

隐式创建一个子线程,并且调用start方法启动线程执行aSelector,和+ detachNewThreadSelector:toTarget:withObject:效果相同。 值得一提的是,这两个方法都是利用- initWithTarget:selector:object:创建子线程,并调用start

NSThread应用

原题出处

- (void)printWhat1 {
    NSLog(@"1");
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"2");
    }];
   
    NSLog(@"3");
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
    
    NSLog(@"5");
    [thread start];
    
    NSLog(@"6");
}

- (void)test {
    NSLog(@"4");
}

打印结果:1 3。 原因分析: 执行到[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];这句代码后,程序卡死。这里和原文理解不同 造成这个结果有2个因素:

  • 当前环境是主线程,但是onThread:thread 中的thread是子线程,这是两个不同的线程,selector的执行会交给RunLoop来执行
  • waitUntilDoneYES,会阻塞主线程

- performSelector:onThread:withObject:waitUntilDone在这个因素同时满足时,会添加一个条件锁,再执行完selector后,会进行解锁。又因为这是一个子线程,RunLoop默认是不开启的,所以selector无法执行,也就无法解锁,那么就卡死在那了

这和原文中的相互等待的理解有所不同

waitUntilDone改成NO之后,不再满足上述条件,所以并不会添加条件锁,使得程序可以继续执行,这才是本质原因。同样的4还是不会打印,也是因为子线程中的RunLoop默认不开启,无法执行selector

posted @ 2021-09-02 16:44  小小个子大个头  阅读(227)  评论(0编辑  收藏  举报