当需要对一个知识详细深入了解的时候,最怕的事情就是,官方文档什么也不说就是光溜溜的代码,就像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