国庆之后真的很糟糕,有时候在深夜偷偷的问自己
是不是不适合写代码?
是不是不适合当程序员?
是不是要转行才能见到未来?
是不是这辈子就打工下去。。。
跑题了 言归正传。今天想跟大家分享下多线程NSThread 接下来也会有
iOS多线程之GCD
iOS多线程之NSOperation
iOS多线程之NSRunLoop
三篇陆续和大家见面
什么是NSThread?
一个NSThread对象代表一个线程,需要手动管理线程的生命周期,处理线程同步等问题 看下官网的一张介绍图:
image.png
翻译下:
如果希望在自己的执行线程中运行Objective-C方法,请使用此类。 当您需要执行冗长的任务但不希望它阻止执行其余应用程序时,线程特别有用。 特别是,您可以使用线程来避免阻塞应用程序的主线程,该主线程处理用户界面和与事件相关的操作。 线程还可用于将大型作业划分为多个较小的作业,这可能会导致多核计算机的性能提升。NSThread类支持类似于NSOperation的语义,用于监视线程的运行时条件。 您可以使用这些语义来取消线程的执行,或确定线程是否仍在执行或已完成其任务。 取消线程需要线程代码的支持; 有关详细信息,请参阅取消说明。
实例方法创建、启动线程
// 1. 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
还有这两种方式:
/*
NSThread *thread = [[NSThread alloc]init];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"thread");
}];
*/
// 2. 启动线程
[thread start]; // 线程一启动,就会在线程thread中执行self的run方法
// 新线程调用方法,里边为需要执行的任务
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
类方法 创建线程后自动启动线程
// 1. 创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
// 新线程调用方法,里边为需要执行的任务
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
隐式创建并启动线程
// 1. 隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];
// 新线程调用方法,里边为需要执行的任务
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
常用的一些属性
//设置线程名字
[thread setName:@"thread - 1"];
//设置线程优先级
[thread setThreadPriority:1.0];
//IOS 8 之后 推荐使用下面这种方式设置线程优先级
//NSQualityOfServiceUserInteractive:最高优先级,用于用户交互事件
//NSQualityOfServiceUserInitiated:次高优先级,用于用户需要马上执行的事件
//NSQualityOfServiceDefault:默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
//NSQualityOfServiceUtility:普通优先级,用于普通任务
//NSQualityOfServiceBackground:最低优先级,用于不重要的任务
[thread setQualityOfService:NSQualityOfServiceUtility];
//判断线程是否是主线程
[thread isMainThread];
//线程状态
//是否已经取消
[thread isCancelled];
//是否已经结束
[thread isFinished];
//是否正在执行
[thread isExecuting];
线程启动、取消、暂停
//线程开始
[thread start];
//线程取消
[thread cancel];
//线程暂停
[thread sleepForTimeInterval:1.0];
//或者下面这种方式 让线程休眠1秒
[thread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
//立即终止除主线程以外所有线程
[NSThread exit];
线程之间的通信
在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法
// 在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes
// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
接下来 我在举个例子 就是很常见的线程间的通信,在子线程里下载图片在主线程刷新UI
/**
* 创建一个线程下载图片
*/
- (void)downloadImageOnChildThread {
// 在创建的子线程中调用downloadImage下载图片
[NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}
/**
* 下载图片,下载完之后回到主线程进行 UI 刷新
*/
- (void)downloadImage {
NSLog(@"current thread -- %@", [NSThread currentThread]);
// 1. imageUrl URL格式化
NSURL *imageUrl = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1539450809191&di=eed853766615f765d622e08803b4e8a9&imgtype=0&src=http%3A%2F%2Ffile03.16sucai.com%2F2017%2F1100%2F16sucai_p20161106032_0c2.JPG"];
// 2. 从 imageUrl 中读取数据(下载图片)
//-- 耗时操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通过二进制 data 创建 image
UIImage *image = [UIImage imageWithData:imageData];
self.imageView.image = image;
// 3. 回到主线程进行图片赋值和界面刷新
// [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}
/**
* 回到主线程进行图片赋值和界面刷新
*/
- (void)refreshOnMainThread:(UIImage *)image {
// 赋值图片到imageview
self.imageView.image = image;
}
以上代码会crash
image.png
设置图片必须在主线程中执行,也就是UI必须在主线程中刷新
// 3. 回到主线程进行图片赋值和界面刷新
[self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
加上这句就OK了
image.png
丝滑的刷新出来了
线程安全和线程同步
线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步:
可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
接下来举两个例子 一个是线程不安全一个是线程安全的
场景:总共有20张体育彩票,有两个售卖体育彩票的窗口,一个是篮球彩票售卖窗口,另一个是足球彩票售卖窗口。两个窗口同时售卖体育彩票,卖完为止。
线程不安全
/**
* 初始化体育彩票票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
// 1. 设置剩余彩票为 50
self.ticketSurplusCount = 50;
// 2. 篮球彩票票票窗口的线程
NSThread *salebBasketballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
salebBasketballWindow.name = @"篮球彩票票票窗口";
// 3. 足球彩票售票窗口的线程
NSThread *salebFootballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
salebFootballWindow.name = @"足球彩票售票窗口";
// 4. 开始售卖体育彩票
[salebBasketballWindow start];
[salebFootballWindow start];
}
/**
* 售卖体育彩票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
//如果还有票,继续售卖
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已卖完,关闭售票窗口
else {
NSLog(@"所有彩票均已售完");
break;
}
}
}
image.png
可以观察以上的数据是错乱的
线程安全
/**
* 初始化体育彩票票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
// 1. 设置剩余彩票为 50
self.ticketSurplusCount = 50;
// 2. 篮球彩票票票窗口的线程
NSThread *salebBasketballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
salebBasketballWindow.name = @"篮球彩票票票窗口";
// 3. 足球彩票售票窗口的线程
NSThread *salebFootballWindow = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
salebFootballWindow.name = @"足球彩票售票窗口";
// 4. 开始售卖体育彩票
[salebBasketballWindow start];
[salebFootballWindow start];
}
/**
* (线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 互斥锁
@synchronized (self) {
//如果还有票,继续售卖
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已卖完,关闭售票窗口
else {
NSLog(@"所有彩票均已售完");
break;
}
}
}
}
image.png
OK 了
解决线程安全就是给线程枷锁,也就是在 在一个线程执行该操作的时候,不允许其他线程进行操作
iOS中的线程锁有很多:
官方上的有:
image.png
@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get
这里用:synchronized
线程的状态转换
当线程创建时
// 1. 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
初始化
// 2. 启动线程
[thread start];
就绪状态
就绪状态
-
如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
-
如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
-
如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。
这张图可以很好的解释线程的状态切换
总结:
NSThread属于轻量级多任务实现方式,可以更加只管的管理线程,需要管理线程的生命周期、同步、加锁问题,会导致一定的性能开销,
注:以上有些素材从网上查找,如有侵权望告知修改。如有不对望指正谢谢。
网友评论