「OC」多线程的学习——NSThread
文章目录
- 「OC」多线程的学习——NSThread
- 线程(process) 和 进程(thread) 的区别
- 多线程
- NSThread
- NSThread的创建
- NSThread的方法
- 常见API
- 线程状态控制方法
- NSThread线程的状态
- NSThread的多线程隐患
- 售票窗口例子
- @synchronize关键字
- NSThread的线程通信
- 线程通信的常用方法
- 参考文章
线程(process) 和 进程(thread) 的区别
- 进程包含线程,一个进程里面可以有一个线程,也可以有多个线程
- 进程和线程都是为了处理并发编程这样的场景,但是进程存在问题,频繁的创建和释放的时候效率低,相比之下线程更轻量,创建和销毁的效率更高
- 操作系统创建进程,要给进程分配资源,进程是操作系统进行资源分配的基本单位,操作系统创建的进程,是要在CPU上执行.而线程是操作系统调度执行的基本单位
- 进程具有独立性,每个经常了个有各自的虚拟地址空间,一个进程挂了不会影响其他进程,而同一个进程中的线程共用同一个内储能空间,一个线程挂了,可能影响到其他线程,甚至导致整个进程崩溃.
在iOS之中App一旦运行,默认就会开启一条线程。这条线程,通常称作为“主线程”。
在iOS应用中主线程的作用一般是:
-
刷新UI;
-
处理UI事件,例如点击、滚动、拖拽。
如果主线程的操作太多、太耗时,就会造成App卡顿现象严重。所以,通常我们都会把耗时的操作放在子线程中进行,获取到结果之后,回到主线程去刷新UI。
多线程
同一时间,单核的CPU只能处理一条线程,也就是只有一条线程在工作。所谓多线程并发(同时)执行,其实是CPU快速的在多线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
当然现在随着多核的CPU的出现,真正的多线程并行也成为了能够真正实现的。
iOS多线程有四种方法:pthread,NSThread,GCD, NSOperation,今天我们主要介绍NSThread的用法,剩下的三种会另外进行介绍
NSThread
NSThread是苹果官方提供面向对象操作线程的技术,简单方便,可以直接操作线程对象,不过需要自己控制线程的生命周期,管理所有的线程活动,如生命周期、线程同步、睡眠等。
NSThread的创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(firstThread:) object:@"Hello, World"];
//设置线程的名字,方便查看
[thread setName:@"firstThread"];
//启动线程
[thread start];
- (void)firstThread:(id)arg
{
NSLog(@"Task %@ %@", [NSThread currentThread], arg);//会将object的内容打印出来
NSLog(@"Thread Task Complete");
}
使用这个Target
这个方法需要使用start
进行开启
以下这个方法是实现创建并且开启线程
[NSThread detachNewThread Selector:@selector(run) toTarget:self withObject:(@"NSTread2")];
// 新线程调用方法,里边为需要执行的任务
- (void)run {
NSLog(@"%@", [NSThread currentThread]);
}
- 隐式创建
// 创建好之后也是直接启动
[self performSelectorInBackground:@selector(doSomething3:) withObject:(@"NSTread3")];
NSThread的方法
常见API
// 获得主线程
+ (NSThread *)mainThread;
// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;
// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;
// 获得当前线程
NSThread *current = [NSThread currentThread];
// 线程的名字——setter方法
- (void)setName:(NSString *)name;
// 线程的名字——getter方法
- (NSString *)name;
// 结束/退出当前线程
+ (void)exit;
// 发送线程取消信号的 最终线程是否结束 由 线程本身决定
- (void)cancel NS_AVAILABLE(10_5, 2_0);
// 启动线程
- (void)start NS_AVAILABLE(10_5, 2_0);
// 获取当前线程优先级
+ (double)threadPriority;
// 设置线程优先级 默认为0.5 取值范围为0.0 - 1.0
// 1.0优先级最高
// 设置优先级
+ (BOOL)setThreadPriority:(double)p;
// 获取指定线程的优先级
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
线程状态控制方法
// 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- (void)start;
// 线程进入阻塞状态
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//强制停止线程 线程进入死亡状态
+ (void)exit;
NSThread线程的状态
线程在程序之中一共有五个状态,具体关系下图所示
总结:
- 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
- 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
- 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。
当我们创建完NSThread,然后将其使用start方法进行操作,就会将创建在内存之中的NSThread对象放在可调度的线程池之中
NSThread的多线程隐患
多线程安全隐患的原因:同一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。
那么当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
我们可以通过下面一系列的图片来,看出NSThread的多线程隐患
通过上图我们发现,当线程A访问数据并对数据进行操作的同时,线程B访问的数据还是没有更新的数据,线程B同样对数据进行操作,当两个线程结束返回时,就会发生数据错乱的问题。
我们可以看出,当线程A访问数据并对数据进行操作的时候,数据被加上一把锁,这个时候其他线程都无法访问数据,知道线程A结束返回数据,线程B此时在访问数据并修改,就不会造成数据错乱了。
售票窗口例子
接下来我们用售票的情景来还原NSThread的多线程隐患,并且在接下来给出解决方案
self.tickets = 20;
NSThread *t1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
t1.name = @"售票员A";
[t1 start];
NSThread *t2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTickets) object:nil];
t2.name = @"售票员B";
[t2 start];
- (void)saleTickets{
while (self.tickets > 0) {
NSThread *thread = [NSThread currentThread];// 获取当前线程
[NSThread sleepForTimeInterval:2];
self.tickets--;
NSLog(@"当前线程:%@\n剩余票数为:%d ",thread.name, self.tickets);
}
}
我们可以看到
这个售票系统在售票的时候会发生错误。这其实是因为在程序运行过程中,如果存在多线程,那么各个线程读写资源就会存在先后、同时读写资源的操作,因为是在不同线程,CPU调度过程中我们无法保证哪个线程会先读写资源,哪个线程后读写资源。因此为了防止数据读写混乱和错误的发生,我们要将线程在读写数据时加锁,这样就能保证操作同一个数据对象的线程只有一个,当这个线程执行完成之后解锁,其他的线程才能操作此数据对象。这就引出我们需要学习的锁
@synchronize关键字
@synchronized(锁对象) {
// 需要锁定的代码
}
我们使用@synchronize关键字实现互斥锁,当在新的线程访问时,如果发现其他线程正在执行锁定的代码,新线程进入休眠
- (void)sellTicket:(NSThread*) thread {
while (self.ticketsCount > 0) {
@synchronized(self) {
NSThread *thread = [NSThread currentThread];
[NSThread sleepForTimeInterval:2];
self.ticketsCount -- ;
NSLog(@"当前线程:%@\n剩余票数为:%d ",thread.name, self.ticketsCount);
}
}
}
但这段代码还是会出现一些问题,就是我们的票数会出现负数,这是因为当多个线程同时检查剩余票数大于0时,都进入了临界区内执行售票操作,但在售票操作执行之前,票数已经被其他线程减少到0以下。
为了解决这个问题,我们可以在关键字@synchronize之中加入一个判断
- (void)saleTickets{
while (true) {
@synchronized(self) {
if (self.tickets <= 0) {
break; // 没有剩余票了,退出循环
}
NSThread *thread = [NSThread currentThread];
[NSThread sleepForTimeInterval:2];
self.tickets--;
NSLog(@"当前线程:%@\n剩余票数为:%d ", thread.name, self.tickets);
}
}
}
我们在检查票数前先获取了锁,确保每次只有一个线程能够进入临界区。同时,我们在临界区内部再次检查剩余票数,以确保在售票时票数仍然大于0。
NSThread的线程通信
线程通信的常用方法
// 返回主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
在多线程的操作当中,我们时常需要在子线程之中获取并下次图片,然后通知主线程更新UI
以下是一个例子
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:self.imageView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
NSURL *url = [NSURL URLWithString:@"https://inews.gtimg.com/om_bt/OE8piEBa-tbqn-wNvWZl8coi4AlzoUD43upEkoAnIkYL8AA/641"];
// 下载图片二进制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 将图片二进制文件转化为image;
UIImage *image = [UIImage imageWithData:data];
[self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
self.imageView.image = image;
}
参考文章
2、NSThread介绍.md
iOS多线程–深度解析
多线程四部曲之NSThread